VAG: rewrite integration and add Audi e-tron/Q4 support (#2968)
This commit is contained in:
parent
b65f05e95f
commit
1d33c54db2
39 changed files with 1109 additions and 488 deletions
37
util/urlvalues/url.go
Normal file
37
util/urlvalues/url.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Package urlvalues provides functions for working with url.Values
|
||||
package urlvalues
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Copy creates a deep copy of url values
|
||||
func Copy(q url.Values) url.Values {
|
||||
res := make(url.Values, len(q))
|
||||
for k, v := range q {
|
||||
res[k] = append([]string{}, v...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Merge copies multiple from url values into to
|
||||
func Merge(to url.Values, from ...url.Values) {
|
||||
for _, vv := range from {
|
||||
for k, v := range vv {
|
||||
to[k] = append([]string{}, v...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Require verifies that url contains the required non-nil values
|
||||
func Require(q url.Values, keys ...string) error {
|
||||
for _, k := range keys {
|
||||
if strings.TrimSpace(q.Get(k)) == "" {
|
||||
return errors.New("missing " + k)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
78
vehicle/audi-etron.go
Normal file
78
vehicle/audi-etron.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/audi/etron"
|
||||
"github.com/evcc-io/evcc/vehicle/id"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/idkproxy"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
|
||||
)
|
||||
|
||||
// https://github.com/TA2k/ioBroker.vw-connect
|
||||
// https://github.com/arjenvrh/audi_connect_ha/blob/master/custom_components/audiconnect/audi_services.py
|
||||
|
||||
// Etron is an api.Vehicle implementation for Audi eTron cars
|
||||
type Etron struct {
|
||||
*embed
|
||||
*id.Provider // provides the api implementations
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("etron", NewEtronFromConfig)
|
||||
}
|
||||
|
||||
// NewEtronFromConfig creates a new vehicle
|
||||
func NewEtronFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
User, Password, VIN string
|
||||
Cache time.Duration
|
||||
Timeout time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
Timeout: request.Timeout,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &Etron{
|
||||
embed: &cc.embed,
|
||||
}
|
||||
|
||||
log := util.NewLogger("etron").Redact(cc.User, cc.Password, cc.VIN)
|
||||
|
||||
// get initial VW identity id_token
|
||||
q, err := vwidentity.Login(log, etron.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// exchange initial VW identity id_token for Audi AAZS token
|
||||
idk := idkproxy.New(log, etron.IDKParams)
|
||||
ats, its, err := service.AAZSTokenSource(log, idk, etron.AZSConfig, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use the etron API for list of vehicles
|
||||
api := etron.NewAPI(log, ats)
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
||||
if err == nil {
|
||||
api := id.NewAPI(log, vag.IDTokenSource(its))
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
v.Provider = id.NewProvider(api, cc.VIN, cc.Cache)
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/audi"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/idkproxy"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
)
|
||||
|
||||
|
|
@ -18,7 +19,6 @@ import (
|
|||
type Audi struct {
|
||||
*embed
|
||||
*vw.Provider // provides the api implementations
|
||||
// audiProvider *audi.Provider
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -46,14 +46,14 @@ func NewAudiFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
log := util.NewLogger("audi").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := vw.NewIdentity(log, audi.AuthClientID, audi.AuthParams, cc.User, cc.Password)
|
||||
|
||||
err := identity.Login()
|
||||
idk := idkproxy.New(log, audi.IDKParams)
|
||||
ts, err := service.MbbTokenSource(log, idk, audi.AuthClientID, audi.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := vw.NewAPI(log, identity, audi.Brand, audi.Country)
|
||||
api := vw.NewAPI(log, ts, audi.Brand, audi.Country)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
|
|
|||
54
vehicle/audi/etron/api.go
Normal file
54
vehicle/audi/etron/api.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package etron
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/shurcooL/graphql"
|
||||
"github.com/thoas/go-funk"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const ApiURI = "https://app-api.live-my.audi.com/vgql/v1/graphql"
|
||||
|
||||
// API is the VW api client
|
||||
type API struct {
|
||||
client *graphql.Client
|
||||
}
|
||||
|
||||
// NewAPI creates a new api client
|
||||
func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
|
||||
ctx := context.WithValue(
|
||||
context.Background(),
|
||||
oauth2.HTTPClient,
|
||||
request.NewHelper(log).Client,
|
||||
)
|
||||
|
||||
v := &API{
|
||||
client: graphql.NewClient(ApiURI, oauth2.NewClient(ctx, ts)),
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Vehicles implements the /vehicles response
|
||||
func (v *API) Vehicles() ([]string, error) {
|
||||
type vehicle struct {
|
||||
VIN, Type, Nickname string
|
||||
}
|
||||
|
||||
var res struct {
|
||||
UserVehicles []vehicle `json:"userVehicles"`
|
||||
}
|
||||
|
||||
var vins []string
|
||||
err := v.client.Query(context.Background(), &res, nil)
|
||||
if err == nil {
|
||||
vins = funk.Map(res.UserVehicles, func(v vehicle) string {
|
||||
return v.VIN
|
||||
}).([]string)
|
||||
}
|
||||
|
||||
return vins, err
|
||||
}
|
||||
20
vehicle/audi/etron/params.go
Normal file
20
vehicle/audi/etron/params.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package etron
|
||||
|
||||
import "net/url"
|
||||
|
||||
// Authorization parameters
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token token"},
|
||||
"client_id": {"f4d0934f-32bf-4ce4-b3c4-699a7049ad26@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"myaudi:///"},
|
||||
"scope": {"openid profile mbb"}, // vin badge birthdate nickname email address phone name picture
|
||||
"prompt": {"login"},
|
||||
"ui_locales": {"de-DE"},
|
||||
}
|
||||
|
||||
var IDKParams = url.Values{
|
||||
"client_id": {"f4d0934f-32bf-4ce4-b3c4-699a7049ad26@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"myaudi:///"},
|
||||
}
|
||||
|
||||
const AZSConfig = "audi"
|
||||
|
|
@ -11,11 +11,16 @@ const (
|
|||
)
|
||||
|
||||
// Authorization parameters
|
||||
var AuthParams = url.Values(map[string][]string{
|
||||
"response_type": {"id_token token"},
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token token"},
|
||||
"client_id": {"09b6cbec-cd19-4589-82fd-363dfa8c24da@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"myaudi:///"},
|
||||
"scope": {"openid profile mbb"}, // vin badge birthdate nickname email address phone name picture
|
||||
"prompt": {"login"},
|
||||
"ui_locales": {"de-DE"},
|
||||
})
|
||||
}
|
||||
|
||||
var IDKParams = url.Values{
|
||||
"client_id": {"09b6cbec-cd19-4589-82fd-363dfa8c24da@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"myaudi:///"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
package id
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// OauthTokenURI is the login uri for ID vehicles
|
||||
OauthTokenURI = "https://login.apps.emea.vwapps.io"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
idtp *vw.IDTokenProvider
|
||||
oauth2.TokenSource
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, user, password string) *Identity {
|
||||
uri := fmt.Sprintf("%s/authorize?%s", OauthTokenURI, AuthParams.Encode())
|
||||
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
idtp: vw.NewIDTokenProvider(log, uri, user, password),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) Login() error {
|
||||
token, err := v.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.TokenSource = oauth.RefreshTokenSource((*oauth2.Token)(&token), v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Identity) login() (Token, error) {
|
||||
q, err := v.idtp.Login()
|
||||
|
||||
if err == nil {
|
||||
for _, k := range []string{"state", "id_token", "access_token", "code"} {
|
||||
if q.Get(k) == "" {
|
||||
err = errors.New("missing " + k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var token Token
|
||||
if err == nil {
|
||||
data := map[string]string{
|
||||
"state": q.Get("state"),
|
||||
"id_token": q.Get("id_token"),
|
||||
"redirect_uri": "weconnect://authenticated",
|
||||
"region": "emea",
|
||||
"access_token": q.Get("access_token"),
|
||||
"authorizationCode": q.Get("code"),
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
uri := fmt.Sprintf("%s/login/v1", OauthTokenURI)
|
||||
req, err = request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
|
||||
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &token)
|
||||
}
|
||||
}
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
uri := fmt.Sprintf("%s/refresh/v1", OauthTokenURI)
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + token.RefreshToken,
|
||||
})
|
||||
|
||||
var res Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
if se, ok := err.(request.StatusError); ok && se.HasStatus(http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden) {
|
||||
res, err = v.login()
|
||||
}
|
||||
|
||||
return (*oauth2.Token)(&res), err
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
package id
|
||||
|
||||
import "net/url"
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
var AuthParams = url.Values(map[string][]string{
|
||||
"github.com/evcc-io/evcc/vehicle/vag/loginapps"
|
||||
)
|
||||
|
||||
const LoginURL = loginapps.BaseURL + "/authorize"
|
||||
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token token"},
|
||||
"client_id": {"a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"weconnect://authenticated"},
|
||||
"scope": {"openid profile badge cars vin"}, // dealers
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/seat"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/tokenrefreshservice"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
)
|
||||
|
||||
|
|
@ -45,14 +46,14 @@ func NewSeatFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
log := util.NewLogger("seat").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := vw.NewIdentity(log, seat.AuthClientID, seat.AuthParams, cc.User, cc.Password)
|
||||
|
||||
err := identity.Login()
|
||||
trs := tokenrefreshservice.New(log, seat.TRSParams)
|
||||
ts, err := service.MbbTokenSource(log, trs, seat.AuthClientID, seat.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := vw.NewAPI(log, identity, seat.Brand, seat.Country)
|
||||
api := vw.NewAPI(log, ts, seat.Brand, seat.Country)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,14 @@ const (
|
|||
)
|
||||
|
||||
// Authorization parameters
|
||||
var AuthParams = url.Values(map[string][]string{
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token"},
|
||||
"client_id": {"50f215ac-4444-4230-9fb1-fe15cd1a9bcc@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"seatconnect://identity-kit/login"},
|
||||
"scope": {"openid profile mbb"}, // cars birthdate nickname address phone
|
||||
})
|
||||
}
|
||||
|
||||
// TokenRefreshService parameters
|
||||
var TRSParams = url.Values{
|
||||
"brand": {"seat"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/skoda"
|
||||
"github.com/evcc-io/evcc/vehicle/skoda/connect"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
)
|
||||
|
||||
// https://github.com/lendy007/skodaconnect
|
||||
|
|
@ -45,22 +46,20 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
var err error
|
||||
log := util.NewLogger("enyaq").Redact(cc.User, cc.Password, cc.VIN)
|
||||
|
||||
if cc.VIN == "" {
|
||||
ts := skoda.NewIdentity(log, skoda.AuthParams, cc.User, cc.Password)
|
||||
if err = ts.Login(); err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
api := skoda.NewAPI(log, ts)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
// use Skoda credentials to resolve list of vehicles
|
||||
ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, skoda.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := skoda.NewAPI(log, ts)
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
||||
// use Connect credentials to build provider
|
||||
if err == nil {
|
||||
ts := skoda.NewIdentity(log, skoda.ConnectAuthParams, cc.User, cc.Password)
|
||||
if err = ts.Login(); err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, connect.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := skoda.NewAPI(log, ts)
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/skoda"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/tokenrefreshservice"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
)
|
||||
|
||||
|
|
@ -45,14 +46,14 @@ func NewSkodaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
log := util.NewLogger("skoda").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := vw.NewIdentity(log, skoda.AuthClientID, skoda.AuthParams, cc.User, cc.Password)
|
||||
|
||||
err := identity.Login()
|
||||
trs := tokenrefreshservice.New(log, skoda.TRSParams)
|
||||
ts, err := service.MbbTokenSource(log, trs, skoda.AuthClientID, skoda.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := vw.NewAPI(log, identity, skoda.Brand, skoda.Country)
|
||||
api := vw.NewAPI(log, ts, skoda.Brand, skoda.Country)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
|
|
|||
13
vehicle/skoda/connect/params.go
Normal file
13
vehicle/skoda/connect/params.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package connect
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Skoda connect api
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token"},
|
||||
"redirect_uri": {"skodaconnect://oidc.login/"},
|
||||
"client_id": {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"},
|
||||
"scope": {"openid profile mbb"}, // phone address cars email birthdate badge dealers driversLicense
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
package skoda
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// OauthTokenURI is the token service uri for Skoda Enyaq vehicles
|
||||
OauthTokenURI = "https://tokenrefreshservice.apps.emea.vwapps.io"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
idtp *vw.IDTokenProvider
|
||||
oauth2.TokenSource
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, query url.Values, user, password string) *Identity {
|
||||
uri := fmt.Sprintf("%s/oidc/v1/authorize?%s", vw.IdentityURI, query.Encode())
|
||||
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
idtp: vw.NewIDTokenProvider(log, uri, user, password),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) Login() error {
|
||||
token, err := v.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.TokenSource = oauth.RefreshTokenSource(&token.Token, v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Identity) login() (vw.Token, error) {
|
||||
q, err := v.idtp.Login()
|
||||
|
||||
if err == nil {
|
||||
for _, k := range []string{"id_token", "code"} {
|
||||
if q.Get(k) == "" {
|
||||
err = errors.New("missing " + k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var token vw.Token
|
||||
if err == nil {
|
||||
data := url.Values(map[string][]string{
|
||||
"auth_code": {q.Get("code")},
|
||||
"id_token": {q.Get("id_token")},
|
||||
"brand": {"skoda"},
|
||||
})
|
||||
|
||||
var req *http.Request
|
||||
uri := fmt.Sprintf("%s/exchangeAuthCode", OauthTokenURI)
|
||||
req, err = request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &token)
|
||||
}
|
||||
|
||||
// check if token response contained error
|
||||
if errT := token.Error(); err != nil && errT != nil {
|
||||
err = fmt.Errorf("token exchange: %w", errT)
|
||||
}
|
||||
}
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
// RefreshToken implements oauth.TokenRefresher
|
||||
func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
uri := fmt.Sprintf("%s/refreshTokens", OauthTokenURI)
|
||||
|
||||
data := url.Values(map[string][]string{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
"brand": {"skoda"},
|
||||
})
|
||||
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
var res vw.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
if se, ok := err.(request.StatusError); ok && se.HasStatus(http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden) {
|
||||
res, err = v.login()
|
||||
}
|
||||
|
||||
return &res.Token, err
|
||||
}
|
||||
|
|
@ -11,17 +11,14 @@ const (
|
|||
)
|
||||
|
||||
// Skoda native api
|
||||
var AuthParams = url.Values(map[string][]string{
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token"},
|
||||
"client_id": {"f9a2359a-b776-46d9-bd0c-db1904343117@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"skodaconnect://oidc.login/"},
|
||||
"scope": {"openid mbb profile"},
|
||||
})
|
||||
}
|
||||
|
||||
// Skoda connect api
|
||||
var ConnectAuthParams = url.Values(map[string][]string{
|
||||
"response_type": {"code id_token"},
|
||||
"redirect_uri": {"skodaconnect://oidc.login/"},
|
||||
"client_id": {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"},
|
||||
"scope": {"openid profile mbb"}, // phone address cars email birthdate badge dealers driversLicense
|
||||
})
|
||||
// TokenRefreshService parameters
|
||||
var TRSParams = url.Values{
|
||||
"brand": {"skoda"},
|
||||
}
|
||||
|
|
|
|||
56
vehicle/vag/aazsproxy/endpoint.go
Normal file
56
vehicle/vag/aazsproxy/endpoint.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package aazsproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const BaseURL = "https://aazsproxy-service.apps.emea.vwapps.io"
|
||||
|
||||
var Endpoint = &oauth2.Endpoint{
|
||||
AuthURL: BaseURL + "/token",
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
func New(log *util.Logger) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
}
|
||||
|
||||
// Exchange exchanges an VAG identity or IDK token for an AAZS token
|
||||
func (v *Service) Exchange(config, token string) (*vag.Token, error) {
|
||||
// TODO make configurable
|
||||
data := struct {
|
||||
Token string `json:"token"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Stage string `json:"stage"`
|
||||
Config string `json:"config"`
|
||||
}{
|
||||
Token: token,
|
||||
GrantType: "id_token",
|
||||
Stage: "live",
|
||||
Config: config,
|
||||
}
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, Endpoint.AuthURL, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// TokenSource creates token source. Token is NOT refreshed but will expire.
|
||||
func (v *Service) TokenSource(token *vag.Token) vag.TokenSource {
|
||||
return vag.RefreshTokenSource(token, nil)
|
||||
}
|
||||
18
vehicle/vag/challenge.go
Normal file
18
vehicle/vag/challenge.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package vag
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
cv "github.com/nirasan/go-oauth-pkce-code-verifier"
|
||||
)
|
||||
|
||||
func ChallengeAndVerifier(q url.Values) func(url.Values) {
|
||||
cvc, _ := cv.CreateCodeVerifier()
|
||||
|
||||
q.Set("code_challenge_method", "S256")
|
||||
q.Set("code_challenge", cvc.CodeChallengeS256())
|
||||
|
||||
return func(q url.Values) {
|
||||
q.Set("code_verifier", cvc.CodeChallengePlain())
|
||||
}
|
||||
}
|
||||
117
vehicle/vag/idkproxy/endpoint.go
Normal file
117
vehicle/vag/idkproxy/endpoint.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package idkproxy
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://idkproxy-service.apps.emea.vwapps.io"
|
||||
WellKnown = BaseURL + "/v1/emea/openid-configuration"
|
||||
)
|
||||
|
||||
var Endpoint = &oauth2.Endpoint{
|
||||
AuthURL: vwidentity.Endpoint.AuthURL,
|
||||
TokenURL: BaseURL + "/v1/emea/token",
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
data url.Values
|
||||
}
|
||||
|
||||
func New(log *util.Logger, q url.Values) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
data: q,
|
||||
}
|
||||
}
|
||||
|
||||
var secret = []byte{55, 24, 256 - 56, 256 - 96, 256 - 72, 256 - 110, 57, 256 - 87, 3, 256 - 86, 256 - 41, 256 - 103, 33, 256 - 30, 99, 103, 81, 125, 256 - 39, 256 - 39, 71, 18, 256 - 107, 256 - 112, 256 - 120, 256 - 12, 256 - 104, 89, 103, 113, 256 - 128, 256 - 91}
|
||||
|
||||
func qmauth(ts int64) string {
|
||||
fmt.Println(ts)
|
||||
|
||||
hash := hmac.New(sha256.New, secret)
|
||||
hash.Write([]byte(strconv.FormatInt(ts, 10)))
|
||||
b := hash.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func qmauthNow() string {
|
||||
ts := time.Now().Unix() / 100
|
||||
return "v1:55f755b0:" + qmauth(ts)
|
||||
}
|
||||
|
||||
// Exchange exchanges an VAG identity token for an IDK token
|
||||
func (v *Service) Exchange(q url.Values) (*vag.Token, error) {
|
||||
if err := urlvalues.Require(q, "code", "code_verifier"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"response_type": {"token id_token"},
|
||||
"code": {q.Get("code")},
|
||||
"code_verifier": {q.Get("code_verifier")},
|
||||
}
|
||||
|
||||
urlvalues.Merge(data, v.data)
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, Endpoint.TokenURL, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": request.FormContent,
|
||||
"Accept": request.JSONContent,
|
||||
"x-qmauth": qmauthNow(),
|
||||
})
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// Refresh refreshes an IDK token
|
||||
func (v *Service) Refresh(token *vag.Token) (*vag.Token, error) {
|
||||
data := url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"response_type": {"token id_token"},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
}
|
||||
|
||||
urlvalues.Merge(data, v.data)
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, Endpoint.TokenURL, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": request.FormContent,
|
||||
"Accept": request.JSONContent,
|
||||
"x-qmauth": qmauthNow(),
|
||||
})
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// TokenSource creates token source. Token is refreshed automatically.
|
||||
func (v *Service) TokenSource(token *vag.Token) vag.TokenSource {
|
||||
return vag.RefreshTokenSource(token, v.Refresh)
|
||||
}
|
||||
12
vehicle/vag/idkproxy/qmauth_test.go
Normal file
12
vehicle/vag/idkproxy/qmauth_test.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package idkproxy
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestQmAuth(t *testing.T) {
|
||||
res := qmauth(16473775)
|
||||
exp := "d1d93982777c56ed2f0d07c4b66435e821fd69a771c3777e43c3cdc219819107"
|
||||
|
||||
if res != exp {
|
||||
t.Errorf("expected %s, got %s", exp, res)
|
||||
}
|
||||
}
|
||||
80
vehicle/vag/loginapps/endpoint.go
Normal file
80
vehicle/vag/loginapps/endpoint.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package loginapps
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://login.apps.emea.vwapps.io"
|
||||
)
|
||||
|
||||
var Endpoint = &oauth2.Endpoint{
|
||||
AuthURL: BaseURL + "/login/v1",
|
||||
TokenURL: BaseURL + "/refresh/v1",
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
func New(log *util.Logger) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Service) Exchange(q url.Values) (*Token, error) {
|
||||
if err := urlvalues.Require(q, "state", "id_token", "access_token", "code"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"region": "emea",
|
||||
"redirect_uri": "weconnect://authenticated",
|
||||
"state": q.Get("state"),
|
||||
"id_token": q.Get("id_token"),
|
||||
"access_token": q.Get("access_token"),
|
||||
"authorizationCode": q.Get("code"),
|
||||
}
|
||||
|
||||
var res Token
|
||||
|
||||
req, err := request.New(http.MethodPost, Endpoint.AuthURL, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (v *Service) Refresh(token *Token) (*Token, error) {
|
||||
req, err := request.New(http.MethodGet, Endpoint.TokenURL, nil, map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + token.RefreshToken,
|
||||
})
|
||||
|
||||
var res Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// RefreshToken implements oauth.TokenRefresher
|
||||
func (v *Service) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
res, err := v.Refresh((*Token)(token))
|
||||
return (*oauth2.Token)(res), err
|
||||
}
|
||||
|
||||
// TokenSource creates a refreshing oauth2 token source
|
||||
func (v *Service) TokenSource(token *Token) oauth2.TokenSource {
|
||||
return oauth.RefreshTokenSource((*oauth2.Token)(token), v)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package id
|
||||
package loginapps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Token is the VW ID token
|
||||
// Token is the loginapps token
|
||||
type Token oauth2.Token
|
||||
|
||||
func (t *Token) UnmarshalJSON(data []byte) error {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package id
|
||||
package loginapps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
84
vehicle/vag/mbb/endpoint.go
Normal file
84
vehicle/vag/mbb/endpoint.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package mbb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://mbboauth-1d.prd.ece.vwg-connect.com"
|
||||
TokenURL = BaseURL + "/mbbcoauth/mobile/oauth2/v1/token"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
clientID string
|
||||
}
|
||||
|
||||
func New(log *util.Logger, clientID string) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
clientID: clientID,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Service) Exchange(q url.Values) (*vag.Token, error) {
|
||||
if err := urlvalues.Require(q, "id_token"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"grant_type": {"id_token"},
|
||||
"token": {q.Get("id_token")},
|
||||
"scope": {"sc2:fal"},
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, TokenURL, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Client-Id": v.clientID,
|
||||
})
|
||||
|
||||
var res vag.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
// check if token response contained error
|
||||
if errT := res.Error(); err != nil && errT != nil {
|
||||
err = fmt.Errorf("token exchange: %w", errT)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (v *Service) Refresh(token *vag.Token) (*vag.Token, error) {
|
||||
data := url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
"scope": {"sc2:fal"},
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, TokenURL, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Client-Id": v.clientID,
|
||||
})
|
||||
|
||||
var res vag.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// TokenSource creates token source. Token is refreshed automatically.
|
||||
func (v *Service) TokenSource(token *vag.Token) vag.TokenSource {
|
||||
return vag.RefreshTokenSource(token, v.Refresh)
|
||||
}
|
||||
42
vehicle/vag/service/azs.go
Normal file
42
vehicle/vag/service/azs.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/aazsproxy"
|
||||
)
|
||||
|
||||
// AAZSTokenSource creates a refreshing token source for use with the AAZS api.
|
||||
// Once the AAZS token expires, it is recreated from the token exchanger (either TokenRefreshService or IDK).
|
||||
// Return values are the AAZS and token exchanger (TRS or IDK) token sources.
|
||||
func AAZSTokenSource(log *util.Logger, tox vag.TokenExchanger, azsConfig string, q url.Values) (vag.TokenSource, vag.TokenSource, error) {
|
||||
token, err := tox.Exchange(q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
trs := tox.TokenSource(token)
|
||||
azs := aazsproxy.New(log)
|
||||
|
||||
mts := vag.MetaTokenSource(func() (*vag.Token, error) {
|
||||
// get TRS token from refreshing TRS token source
|
||||
itoken, err := trs.TokenEx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// exchange TRS id_token for AAZS token
|
||||
atoken, err := azs.Exchange(azsConfig, itoken.IDToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return atoken, err
|
||||
|
||||
// produce tokens from refresh MBB token source
|
||||
}, azs.TokenSource)
|
||||
|
||||
return mts, trs, nil
|
||||
}
|
||||
47
vehicle/vag/service/mbb.go
Normal file
47
vehicle/vag/service/mbb.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/mbb"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
|
||||
)
|
||||
|
||||
// MbbTokenSource creates a refreshing token source for use with the MBB api.
|
||||
// Once the MBB token expires, it is recreated from the token exchanger (either TokenRefreshService or IDK)
|
||||
func MbbTokenSource(log *util.Logger, tox vag.TokenExchanger, clientID string, q url.Values, user, password string) (vag.TokenSource, error) {
|
||||
q, err := vwidentity.Login(log, q, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := tox.Exchange(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trs := tox.TokenSource(token)
|
||||
mbb := mbb.New(log, clientID)
|
||||
|
||||
mts := vag.MetaTokenSource(func() (*vag.Token, error) {
|
||||
// get TRS token from refreshing TRS token source
|
||||
itoken, err := trs.TokenEx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// exchange TRS id_token for MBB token
|
||||
mtoken, err := mbb.Exchange(url.Values{"id_token": {itoken.IDToken}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mtoken, err
|
||||
|
||||
// produce tokens from refresh MBB token source
|
||||
}, mbb.TokenSource)
|
||||
|
||||
return mts, nil
|
||||
}
|
||||
25
vehicle/vag/service/tokenrefreshservice.go
Normal file
25
vehicle/vag/service/tokenrefreshservice.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/tokenrefreshservice"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TokenRefreshServiceTokenSource(log *util.Logger, data, q url.Values, user, password string) (oauth2.TokenSource, error) {
|
||||
q, err := vwidentity.Login(log, q, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trs := tokenrefreshservice.New(log, data)
|
||||
token, err := trs.Exchange(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return trs.TokenSource(token), nil
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vw
|
||||
package vag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -11,13 +11,15 @@ import (
|
|||
// Token is an OAuth2-compatible token that supports the expires_in attribute
|
||||
type Token struct {
|
||||
oauth2.Token
|
||||
err error
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
err error
|
||||
}
|
||||
|
||||
func (t *Token) UnmarshalJSON(data []byte) error {
|
||||
var s struct {
|
||||
oauth2.Token
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
Error *string
|
||||
ErrorDescription *string `json:"error_description"`
|
||||
}
|
||||
|
|
@ -25,6 +27,7 @@ func (t *Token) UnmarshalJSON(data []byte) error {
|
|||
err := json.Unmarshal(data, &s)
|
||||
if err == nil {
|
||||
t.Token = s.Token
|
||||
t.IDToken = s.IDToken
|
||||
|
||||
if s.Expiry.IsZero() && s.ExpiresIn != 0 {
|
||||
t.Expiry = time.Now().Add(time.Second * time.Duration(s.ExpiresIn))
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vw
|
||||
package vag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -7,12 +7,16 @@ import (
|
|||
|
||||
func TestUnmarshalJSON(t *testing.T) {
|
||||
var tok Token
|
||||
data := `{"access_token":"access","refresh_token":"refresh","token_type":"bearer","expires_in":3600}`
|
||||
data := `{"id_token":"id","access_token":"access","refresh_token":"refresh","token_type":"bearer","expires_in":3600}`
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &tok); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if tok.IDToken != "id" {
|
||||
t.Error("IDToken")
|
||||
}
|
||||
|
||||
if tok.AccessToken != "access" {
|
||||
t.Error("AccessToken")
|
||||
}
|
||||
75
vehicle/vag/tokenrefreshservice/endpoint.go
Normal file
75
vehicle/vag/tokenrefreshservice/endpoint.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package tokenrefreshservice
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://tokenrefreshservice.apps.emea.vwapps.io"
|
||||
CodeExchangeURL = BaseURL + "/exchangeAuthCode"
|
||||
RefreshTokenURL = BaseURL + "/refreshTokens"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
data url.Values
|
||||
}
|
||||
|
||||
func New(log *util.Logger, q url.Values) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
data: q,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Service) Exchange(q url.Values) (*vag.Token, error) {
|
||||
if err := urlvalues.Require(q, "id_token", "code"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"auth_code": {q.Get("code")},
|
||||
"id_token": {q.Get("id_token")},
|
||||
}
|
||||
|
||||
urlvalues.Merge(data, v.data, q)
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, CodeExchangeURL, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (v *Service) Refresh(token *vag.Token) (*vag.Token, error) {
|
||||
data := url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
}
|
||||
|
||||
urlvalues.Merge(data, v.data)
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, RefreshTokenURL, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// TokenSource creates token source. Token is refreshed automatically.
|
||||
func (v *Service) TokenSource(token *vag.Token) vag.TokenSource {
|
||||
return vag.RefreshTokenSource(token, v.Refresh)
|
||||
}
|
||||
144
vehicle/vag/tokensource.go
Normal file
144
vehicle/vag/tokensource.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package vag
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// TokenSource is a VAG token source compatible with oauth2.TokenSource
|
||||
type TokenSource interface {
|
||||
// Token returns an OAuth2 compatible token (id_token omitted)
|
||||
Token() (*oauth2.Token, error)
|
||||
// TokenEx returns the extended VAG token (id_token included)
|
||||
TokenEx() (*Token, error)
|
||||
}
|
||||
|
||||
// TokenExchanger exchanges a VW identity response into a (refreshing) VAG token source
|
||||
type TokenExchanger interface {
|
||||
Exchange(q url.Values) (*Token, error)
|
||||
TokenSource(token *Token) TokenSource
|
||||
}
|
||||
|
||||
// TokenRefresher refreshes a token
|
||||
type TokenRefresher func(*Token) (*Token, error)
|
||||
|
||||
var _ TokenSource = (*tokenSource)(nil)
|
||||
|
||||
type tokenSource struct {
|
||||
mu sync.Mutex
|
||||
token *Token
|
||||
new TokenRefresher
|
||||
}
|
||||
|
||||
func RefreshTokenSource(token *Token, refresher TokenRefresher) *tokenSource {
|
||||
return &tokenSource{token: token, new: refresher}
|
||||
}
|
||||
|
||||
// Token returns an oauth2 token or an error
|
||||
func (ts *tokenSource) Token() (*oauth2.Token, error) {
|
||||
token, err := ts.TokenEx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &token.Token, err
|
||||
}
|
||||
|
||||
func (ts *tokenSource) TokenEx() (*Token, error) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
|
||||
var err error
|
||||
if time.Until(ts.token.Expiry) < time.Minute {
|
||||
var token *Token
|
||||
if token, err = ts.new(ts.token); err == nil {
|
||||
err = ts.mergeToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
return ts.token, err
|
||||
}
|
||||
|
||||
// mergeToken updates a token while preventing wiping the refresh token
|
||||
func (ts *tokenSource) mergeToken(t *Token) error {
|
||||
return mergo.Merge(ts.token, t, mergo.WithOverride)
|
||||
}
|
||||
|
||||
type metaTokenSource struct {
|
||||
mu sync.Mutex
|
||||
ts TokenSource
|
||||
newT func() (*Token, error)
|
||||
newTS func(*Token) TokenSource
|
||||
}
|
||||
|
||||
// MetaTokenSource creates a token source that is created using the
|
||||
// `newTS` function or recreated once it fails to return tokens.
|
||||
// The recreation uses a new bootstrap token provided by the `newT` function.
|
||||
func MetaTokenSource(newT func() (*Token, error), newTS func(*Token) TokenSource) *metaTokenSource {
|
||||
return &metaTokenSource{
|
||||
newT: newT,
|
||||
newTS: newTS,
|
||||
}
|
||||
}
|
||||
|
||||
// Token returns an oauth2 token or an error
|
||||
func (ts *metaTokenSource) Token() (*oauth2.Token, error) {
|
||||
token, err := ts.TokenEx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &token.Token, err
|
||||
}
|
||||
|
||||
// Token returns a vag token or an error
|
||||
func (ts *metaTokenSource) TokenEx() (*Token, error) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
|
||||
if ts.ts == nil {
|
||||
token, err := ts.newT()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts.ts = ts.newTS(token)
|
||||
}
|
||||
|
||||
token, err := ts.ts.TokenEx()
|
||||
if err != nil {
|
||||
// token source doesn't work anymore, reset it
|
||||
ts.ts = nil
|
||||
}
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
type idTokenSource struct {
|
||||
ts TokenSource
|
||||
}
|
||||
|
||||
// IDTokenSource provides an oauth2 token source with access_token populated from vag id_token
|
||||
func IDTokenSource(ts TokenSource) oauth2.TokenSource {
|
||||
return &idTokenSource{ts}
|
||||
}
|
||||
|
||||
// Token returns an oauth2 token or an error
|
||||
func (ts *idTokenSource) Token() (*oauth2.Token, error) {
|
||||
token, err := ts.ts.TokenEx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idToken := &oauth2.Token{
|
||||
TokenType: token.TokenType,
|
||||
AccessToken: token.IDToken,
|
||||
Expiry: token.Expiry,
|
||||
}
|
||||
|
||||
return idToken, err
|
||||
}
|
||||
40
vehicle/vag/tokensource_test.go
Normal file
40
vehicle/vag/tokensource_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package vag
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
ts := &tokenSource{
|
||||
token: &Token{
|
||||
IDToken: "id",
|
||||
Token: oauth2.Token{
|
||||
AccessToken: "access",
|
||||
RefreshToken: "refresh",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := &Token{
|
||||
IDToken: "foo",
|
||||
Token: oauth2.Token{
|
||||
AccessToken: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
if err := ts.mergeToken(r); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if ts.token.IDToken != "foo" {
|
||||
t.Error("unexpected id token", ts.token)
|
||||
}
|
||||
if ts.token.AccessToken != "bar" {
|
||||
t.Error("unexpected access token", ts.token)
|
||||
}
|
||||
if ts.token.RefreshToken != "refresh" {
|
||||
t.Error("unexpected refresh token", ts.token)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vw
|
||||
package vwidentity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -8,38 +8,66 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// IDTokenProvider provides the identity.vwgroup.io login token source
|
||||
type IDTokenProvider struct {
|
||||
log *util.Logger
|
||||
*request.Helper
|
||||
uri, user, password string
|
||||
const (
|
||||
BaseURL = "https://identity.vwgroup.io"
|
||||
WellKnown = BaseURL + "/.well-known/openid-configuration"
|
||||
)
|
||||
|
||||
var Endpoint = &oauth2.Endpoint{
|
||||
AuthURL: BaseURL + "/oidc/v1/authorize",
|
||||
TokenURL: BaseURL + "/oidc/v1/token",
|
||||
}
|
||||
|
||||
// NewIDTokenProvider creates VW identity
|
||||
func NewIDTokenProvider(log *util.Logger, uri, user, password string) *IDTokenProvider {
|
||||
v := &IDTokenProvider{
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
uri: uri,
|
||||
user: user,
|
||||
password: password,
|
||||
// Login performs VW identity login with optional code challenge
|
||||
func Login(log *util.Logger, q url.Values, user, password string) (url.Values, error) {
|
||||
return LoginWithAuthURL(log, Endpoint.AuthURL, q, user, password)
|
||||
}
|
||||
|
||||
func LoginWithAuthURL(log *util.Logger, uri string, q url.Values, user, password string) (url.Values, error) {
|
||||
var verify func(url.Values)
|
||||
|
||||
// add code challenge
|
||||
q = urlvalues.Copy(q)
|
||||
if rt := q.Get("response_type"); strings.Contains(rt, "code") {
|
||||
verify = vag.ChallengeAndVerifier(q)
|
||||
}
|
||||
|
||||
return v
|
||||
uri = fmt.Sprintf("%s?%s", uri, q.Encode())
|
||||
|
||||
vwi := New(log)
|
||||
q, err := vwi.Login(uri, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if verify != nil {
|
||||
verify(q)
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
func New(log *util.Logger) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
}
|
||||
|
||||
// Login performs the identity.vwgroup.io login
|
||||
func (v *IDTokenProvider) Login() (url.Values, error) {
|
||||
if v.user == "" || v.password == "" {
|
||||
return nil, api.ErrMissingCredentials
|
||||
}
|
||||
|
||||
func (v *Service) Login(uri, user, password string) (url.Values, error) {
|
||||
// track cookies and don't follow redirects
|
||||
jar, _ := cookiejar.New(&cookiejar.Options{
|
||||
PublicSuffixList: publicsuffix.List,
|
||||
|
|
@ -55,14 +83,14 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
var vars FormVars
|
||||
|
||||
// add nonce and state
|
||||
query := url.Values{
|
||||
"nonce": []string{util.RandomString(43)},
|
||||
"state": []string{util.RandomString(43)},
|
||||
"state": []string{uuid.NewString()},
|
||||
}
|
||||
uri := v.uri + "&" + query.Encode()
|
||||
|
||||
var vars FormVars
|
||||
uri = uri + "&" + query.Encode()
|
||||
|
||||
// GET identity.vwgroup.io/signin-service/v1/signin/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com?relayState=15404cb51c8b4cc5efeee1d2c2a73e5b41562faa
|
||||
resp, err := v.Get(uri)
|
||||
|
|
@ -75,14 +103,14 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
|
|||
|
||||
// POST identity.vwgroup.io/signin-service/v1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com/login/identifier
|
||||
if err == nil {
|
||||
data := url.Values(map[string][]string{
|
||||
data := url.Values{
|
||||
"_csrf": {vars.Inputs["_csrf"]},
|
||||
"relayState": {vars.Inputs["relayState"]},
|
||||
"hmac": {vars.Inputs["hmac"]},
|
||||
"email": {v.user},
|
||||
})
|
||||
"email": {user},
|
||||
}
|
||||
|
||||
uri = IdentityURI + vars.Action
|
||||
uri = BaseURL + vars.Action
|
||||
if resp, err = v.PostForm(uri, data); err == nil {
|
||||
if params, err = ParseCredentialsPage(resp.Body); err == nil && params.TemplateModel.Error != "" {
|
||||
err = errors.New(params.TemplateModel.Error)
|
||||
|
|
@ -93,13 +121,13 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
|
|||
|
||||
// POST identity.vwgroup.io/signin-service/v1/b7a5bb47-f875-47cf-ab83-2ba3bf6bb738@apps_vw-dilab_com/login/authenticate
|
||||
if err == nil {
|
||||
data := url.Values(map[string][]string{
|
||||
data := url.Values{
|
||||
"_csrf": {params.CsrfToken},
|
||||
"relayState": {params.TemplateModel.RelayState},
|
||||
"hmac": {params.TemplateModel.Hmac},
|
||||
"email": {v.user},
|
||||
"password": {v.password},
|
||||
})
|
||||
"email": {user},
|
||||
"password": {password},
|
||||
}
|
||||
|
||||
// reuse url from identifier step before
|
||||
uri = strings.ReplaceAll(uri, params.TemplateModel.IdentifierUrl, params.TemplateModel.PostAction)
|
||||
|
|
@ -118,9 +146,10 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
|
|||
}
|
||||
|
||||
if u := resp.Request.URL.Query().Get("updated"); err == nil && u != "" {
|
||||
v.log.WARN.Println("accepting updated tos", u)
|
||||
if resp, err = v.postTos(resp.Request.URL.String()); err == nil {
|
||||
resp.Body.Close()
|
||||
} else {
|
||||
err = fmt.Errorf("updated ToS: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +170,7 @@ func (v *IDTokenProvider) Login() (url.Values, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
func (v *IDTokenProvider) postTos(uri string) (*http.Response, error) {
|
||||
func (v *Service) postTos(uri string) (*http.Response, error) {
|
||||
var vars FormVars
|
||||
resp, err := v.Get(uri)
|
||||
if err == nil {
|
||||
|
|
@ -154,7 +183,7 @@ func (v *IDTokenProvider) postTos(uri string) (*http.Response, error) {
|
|||
data.Set(k, v)
|
||||
}
|
||||
|
||||
uri := IdentityURI + vars.Action
|
||||
uri := BaseURL + vars.Action
|
||||
resp, err = v.PostForm(uri, data)
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vw
|
||||
package vwidentity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/id"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/loginapps"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
|
||||
)
|
||||
|
||||
// https://github.com/TA2k/ioBroker.vw-connect
|
||||
|
|
@ -43,14 +44,19 @@ func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
log := util.NewLogger("id").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := id.NewIdentity(log, cc.User, cc.Password)
|
||||
|
||||
err := identity.Login()
|
||||
q, err := vwidentity.LoginWithAuthURL(log, id.LoginURL, id.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := id.NewAPI(log, identity)
|
||||
apps := loginapps.New(log)
|
||||
token, err := apps.Exchange(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := id.NewAPI(log, apps.TokenSource(token))
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/tokenrefreshservice"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
)
|
||||
|
||||
|
|
@ -45,13 +46,13 @@ func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
|
||||
log := util.NewLogger("vw").Redact(cc.User, cc.Password, cc.VIN)
|
||||
|
||||
identity := vw.NewIdentity(log, vw.AuthClientID, vw.AuthParams, cc.User, cc.Password)
|
||||
err := identity.Login()
|
||||
trs := tokenrefreshservice.New(log, vw.TRSParams)
|
||||
ts, err := service.MbbTokenSource(log, trs, vw.AuthClientID, vw.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := vw.NewAPI(log, identity, vw.Brand, vw.Country)
|
||||
api := vw.NewAPI(log, ts, vw.Brand, vw.Country)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
package vw
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// ChallengeVerifier returns a challenge/verifier base64-encoded code combination
|
||||
func ChallengeVerifier() (string, string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
n, err := rand.Read(bytes)
|
||||
if n != 32 || err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
verifier := base64.RawURLEncoding.EncodeToString(bytes)
|
||||
sha := sha256.Sum256([]byte(verifier))
|
||||
challenge := base64.RawURLEncoding.EncodeToString(sha[:])
|
||||
|
||||
return challenge, verifier, nil
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
package vw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// https://identity.vwgroup.io/.well-known/openid-configuration
|
||||
|
||||
const (
|
||||
// IdentityURI is the VW OIDC identity provider uri
|
||||
IdentityURI = "https://identity.vwgroup.io"
|
||||
|
||||
// OauthTokenURI is used for refreshing tokens
|
||||
OauthTokenURI = "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token"
|
||||
|
||||
// OauthRevokeURI is used for revoking tokens
|
||||
OauthRevokeURI = "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/revoke"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
oauth2.TokenSource
|
||||
idtp *IDTokenProvider
|
||||
clientID string
|
||||
refresher oauth.TokenRefresher
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, clientID string, query url.Values, user, password string) *Identity {
|
||||
uri := fmt.Sprintf("%s/oidc/v1/authorize?%s", IdentityURI, query.Encode())
|
||||
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
clientID: clientID,
|
||||
idtp: NewIDTokenProvider(log, uri, user, password),
|
||||
refresher: NewTokenRefresher(log, clientID),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) Login() error {
|
||||
token, err := v.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.TokenSource = oauth.RefreshTokenSource(&token.Token, v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// login performs the login using the brand-specific subflow for obtaining the
|
||||
// id token and finally exchanges the id token for access and refresh tokens
|
||||
func (v *Identity) login() (Token, error) {
|
||||
q, err := v.idtp.Login()
|
||||
|
||||
if err == nil && q.Get("id_token") == "" {
|
||||
err = errors.New("missing id_token")
|
||||
}
|
||||
|
||||
var token Token
|
||||
if err == nil {
|
||||
data := url.Values(map[string][]string{
|
||||
"grant_type": {"id_token"},
|
||||
"scope": {"sc2:fal"},
|
||||
"token": {q.Get("id_token")},
|
||||
})
|
||||
|
||||
var req *http.Request
|
||||
req, err = request.New(http.MethodPost, OauthTokenURI, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Client-Id": v.clientID,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &token)
|
||||
}
|
||||
|
||||
// check if token response contained error
|
||||
if errT := token.Error(); err != nil && errT != nil {
|
||||
err = fmt.Errorf("token exchange: %w", errT)
|
||||
}
|
||||
}
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
// RefreshToken implements oauth.TokenRefresher
|
||||
func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
token, err := v.refresher.RefreshToken(token)
|
||||
if err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// re-login
|
||||
var res Token
|
||||
if se, ok := err.(request.StatusError); ok && se.HasStatus(http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden) {
|
||||
res, err = v.login()
|
||||
}
|
||||
|
||||
return &res.Token, err
|
||||
}
|
||||
|
|
@ -11,9 +11,14 @@ const (
|
|||
)
|
||||
|
||||
// Authorization parameters
|
||||
var AuthParams = url.Values(map[string][]string{
|
||||
"response_type": {"id_token token"},
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token token"},
|
||||
"client_id": {"9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"carnet://identity-kit/login"},
|
||||
"scope": {"openid profile mbb"}, // cars birthdate nickname address phone
|
||||
})
|
||||
}
|
||||
|
||||
// TokenRefreshService parameters
|
||||
var TRSParams = url.Values{
|
||||
"brand": {"vw"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package vw
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// TokenRefresher performs the MBBOauth token refresh without re-login
|
||||
type TokenRefresher struct {
|
||||
*request.Helper
|
||||
clientID string
|
||||
}
|
||||
|
||||
// NewTokenRefresher creates an MBBOauth token refresher
|
||||
func NewTokenRefresher(log *util.Logger, clientID string) *TokenRefresher {
|
||||
return &TokenRefresher{
|
||||
Helper: request.NewHelper(log),
|
||||
clientID: clientID,
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshToken implements oauth.TokenRefresher
|
||||
func (v *TokenRefresher) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
data := url.Values(map[string][]string{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
"scope": {"sc2:fal"},
|
||||
})
|
||||
|
||||
req, err := request.New(http.MethodPost, OauthTokenURI, strings.NewReader(data.Encode()), map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Client-Id": v.clientID,
|
||||
})
|
||||
|
||||
var res Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res.Token, err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue