Porsche: switch identity provider (#8546)
This commit is contained in:
parent
6f90409a3c
commit
2ffb050fb4
7 changed files with 133 additions and 155 deletions
|
|
@ -5,6 +5,11 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// DontFollow is a redirect policy that does not follow redirects
|
||||
func DontFollow(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
type InterceptResult = func() (string, error)
|
||||
|
||||
// InterceptRedirect captures a redirect url parameter
|
||||
|
|
@ -181,7 +181,7 @@ func (v *Identity) brandLogin(cookieClient *request.Helper, user, password strin
|
|||
|
||||
req, err = request.New(http.MethodPost, action, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err == nil {
|
||||
cookieClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } // don't follow redirects
|
||||
cookieClient.CheckRedirect = request.DontFollow
|
||||
if resp, err = cookieClient.Do(req); err == nil {
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ func (v *Identity) brandLogin(cookieClient *request.Helper, user, password strin
|
|||
req, err = request.New(http.MethodPost, v.config.URI+SilentSigninURL, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err == nil {
|
||||
req.Header.Set("ccsp-service-id", v.config.CCSPServiceID)
|
||||
cookieClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } // don't follow redirects
|
||||
cookieClient.CheckRedirect = request.DontFollow
|
||||
|
||||
var res struct {
|
||||
RedirectUrl string `json:"redirectUrl"`
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
|||
}
|
||||
|
||||
func (v *Identity) login() (*oauth2.Token, error) {
|
||||
// don't follow redirects
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
|
||||
v.Client.CheckRedirect = request.DontFollow
|
||||
defer func() { v.Client.CheckRedirect = nil }()
|
||||
|
||||
cv, err := cv.CreateCodeVerifier()
|
||||
|
|
|
|||
|
|
@ -144,7 +144,6 @@ func (v *Identity) login() (*oauth.Token, error) {
|
|||
}
|
||||
|
||||
tok, err := OAuth2Config.Exchange(ctx, code,
|
||||
oauth2.SetAuthURLParam("grant_type", "authorization_code"),
|
||||
oauth2.SetAuthURLParam("code_verifier", cv.CodeChallengePlain()),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
log := util.NewLogger("porsche").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := porsche.NewIdentity(log, cc.User, cc.Password)
|
||||
identity := porsche.NewIdentity(log)
|
||||
|
||||
err := identity.Login()
|
||||
err := identity.Login(porsche.OAuth2Config, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
api := porsche.NewAPI(log, identity.DefaultSource)
|
||||
api := porsche.NewAPI(log, identity)
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, func() ([]string, error) {
|
||||
vehicles, err := api.Vehicles()
|
||||
|
|
@ -66,13 +66,18 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
// get eMobility capabilities
|
||||
emobility := porsche.NewEmobilityAPI(log, identity.EmobilitySource)
|
||||
capabilities, err := emobility.Capabilities(cc.VIN)
|
||||
emobIdentity := porsche.NewIdentity(log)
|
||||
if err := emobIdentity.Login(porsche.EmobilityOAuth2Config, cc.User, cc.Password); err != nil {
|
||||
return nil, fmt.Errorf("emobility login failed: %w", err)
|
||||
}
|
||||
|
||||
emobApi := porsche.NewEmobilityAPI(log, emobIdentity)
|
||||
capabilities, err := emobApi.Capabilities(cc.VIN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := porsche.NewProvider(log, api, emobility, cc.VIN, capabilities.CarModel, cc.Cache)
|
||||
provider := porsche.NewProvider(log, api, emobApi, cc.VIN, capabilities.CarModel, cc.Cache)
|
||||
|
||||
v := &Porsche{
|
||||
embed: &cc.embed,
|
||||
|
|
|
|||
|
|
@ -2,203 +2,167 @@ package porsche
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
|
||||
"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"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
OAuthURI = "https://login.porsche.com"
|
||||
OAuthURI = "https://identity.porsche.com"
|
||||
)
|
||||
|
||||
// https://login.porsche.com/.well-known/openid-configuration
|
||||
// https://identity.porsche.com/.well-known/openid-configuration
|
||||
var (
|
||||
endpoint = oauth2.Endpoint{
|
||||
AuthURL: OAuthURI + "/authorize",
|
||||
TokenURL: OAuthURI + "/oauth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
}
|
||||
|
||||
OAuth2Config = &oauth2.Config{
|
||||
ClientID: "4mPO3OE5Srjb1iaUGWsbqKBvvesya8oA",
|
||||
RedirectURL: "https://my.porsche.com/core/de/de_DE/",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: OAuthURI + "/as/authorization.oauth2",
|
||||
TokenURL: OAuthURI + "/as/token.oauth2",
|
||||
},
|
||||
Scopes: []string{"openid"},
|
||||
ClientID: "UYsK00My6bCqJdbQhTQ0PbWmcSdIAMig",
|
||||
RedirectURL: "https://my.porsche.com/",
|
||||
Endpoint: endpoint,
|
||||
Scopes: []string{"openid", "offline_access"},
|
||||
}
|
||||
|
||||
EmobilityOAuth2Config = &oauth2.Config{
|
||||
ClientID: "NJOxLv4QQNrpZnYQbb7mCvdiMxQWkHDq",
|
||||
ClientID: OAuth2Config.ClientID,
|
||||
RedirectURL: "https://my.porsche.com/myservices/auth/auth.html",
|
||||
Endpoint: OAuth2Config.Endpoint,
|
||||
Scopes: []string{"openid"},
|
||||
Endpoint: endpoint,
|
||||
Scopes: []string{"openid", "offline_access"},
|
||||
}
|
||||
)
|
||||
|
||||
// Identity is the Porsche Identity client
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
user, password string
|
||||
defaultToken, emobilityToken *oauth2.Token
|
||||
DefaultSource, EmobilitySource oauth2.TokenSource
|
||||
oauth2.TokenSource
|
||||
}
|
||||
|
||||
// NewIdentity creates Porsche identity
|
||||
func NewIdentity(log *util.Logger, user, password string) *Identity {
|
||||
func NewIdentity(log *util.Logger) *Identity {
|
||||
v := &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
user: user,
|
||||
password: password,
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *Identity) Login() error {
|
||||
_, err := v.RefreshToken(nil)
|
||||
|
||||
if err == nil {
|
||||
v.DefaultSource = oauth.RefreshTokenSource(v.defaultToken, v)
|
||||
v.EmobilitySource = oauth.RefreshTokenSource(v.emobilityToken, &emobilityAdapter{v})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshToken performs new login and creates default and emobility tokens
|
||||
func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
||||
jar, err := cookiejar.New(&cookiejar.Options{
|
||||
PublicSuffixList: publicsuffix.List,
|
||||
})
|
||||
func (v *Identity) Login(oc *oauth2.Config, user, password string) error {
|
||||
cv, err := cv.CreateCodeVerifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// track cookies and follow all (>10) redirects
|
||||
v.Client.Jar = jar
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
state := lo.RandomString(16, lo.AlphanumericCharset)
|
||||
uri := OAuth2Config.AuthCodeURL(state,
|
||||
oauth2.SetAuthURLParam("audience", ApiURI),
|
||||
oauth2.SetAuthURLParam("ui_locales", "de-DE"),
|
||||
oauth2.SetAuthURLParam("code_challenge", cv.CodeChallengeS256()),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||
)
|
||||
|
||||
v.Client.Jar, _ = cookiejar.New(nil)
|
||||
v.Client.CheckRedirect = request.DontFollow
|
||||
defer func() {
|
||||
v.Client.Jar = nil
|
||||
v.Client.CheckRedirect = nil
|
||||
}()
|
||||
|
||||
preLogin := url.Values{
|
||||
"sec": []string{""},
|
||||
"resume": []string{""},
|
||||
"thirdPartyId": []string{""},
|
||||
"state": []string{""},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
"keeploggedin": []string{"false"},
|
||||
}
|
||||
|
||||
// get the login page
|
||||
uri := fmt.Sprintf("%s/auth/api/v1/de/de_DE/public/login", OAuthURI)
|
||||
resp, err := v.PostForm(uri, preLogin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
query, err := url.ParseQuery(resp.Request.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataLoginAuth := url.Values{
|
||||
"sec": []string{query.Get("sec")},
|
||||
"resume": []string{query.Get("resume")},
|
||||
"thirdPartyId": []string{query.Get("thirdPartyID")},
|
||||
"state": []string{query.Get("state")},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
"keeploggedin": []string{"false"},
|
||||
}
|
||||
|
||||
// process the auth so the session is authenticated
|
||||
resp, err = v.PostForm(uri, dataLoginAuth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// get the token for the generic API
|
||||
token, err := v.fetchToken(OAuth2Config)
|
||||
if err == nil {
|
||||
v.defaultToken = token
|
||||
|
||||
if token, err = v.fetchToken(EmobilityOAuth2Config); err == nil {
|
||||
v.emobilityToken = token
|
||||
}
|
||||
}
|
||||
|
||||
return v.defaultToken, err
|
||||
}
|
||||
|
||||
func (v *Identity) fetchToken(oc *oauth2.Config) (*oauth2.Token, error) {
|
||||
cv, err := cv.CreateCodeVerifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := oc.AuthCodeURL("uvobn7XJs1", oauth2.AccessTypeOffline,
|
||||
oauth2.SetAuthURLParam("code_challenge", cv.CodeChallengeS256()),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||
oauth2.SetAuthURLParam("country", "de"),
|
||||
oauth2.SetAuthURLParam("locale", "de_DE"),
|
||||
)
|
||||
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if req.URL.Scheme != "https" {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := v.Client.Get(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
u, err := url.Parse(resp.Header.Get("Location"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
for _, p := range []string{"client_id", "code_challenge", "scope", "protocol"} {
|
||||
query.Del(p)
|
||||
}
|
||||
for k, v := range map[string]string{
|
||||
"connection": "Username-Password-Authentication",
|
||||
"tenant": "porsche-production",
|
||||
"sec": "high",
|
||||
} {
|
||||
query.Set(k, v)
|
||||
}
|
||||
query.Set("client_id", oc.ClientID)
|
||||
query.Set("username", user)
|
||||
query.Set("password", password)
|
||||
|
||||
uri = fmt.Sprintf("%s/usernamepassword/login", OAuthURI)
|
||||
resp, err = v.PostForm(uri, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var res struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err == nil && res.Description != "" {
|
||||
return errors.New(res.Description)
|
||||
}
|
||||
return fmt.Errorf("unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query = make(url.Values)
|
||||
doc.Find("input[type=hidden]").Each(func(_ int, el *goquery.Selection) {
|
||||
if name, ok := el.Attr("name"); ok {
|
||||
val, _ := el.Attr("value")
|
||||
query.Set(name, val)
|
||||
}
|
||||
})
|
||||
|
||||
var param request.InterceptResult
|
||||
v.Client.CheckRedirect, param = request.InterceptRedirect("code", true)
|
||||
|
||||
uri = fmt.Sprintf("%s/login/callback", OAuthURI)
|
||||
resp, err = v.PostForm(uri, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
query, err := url.ParseQuery(resp.Request.URL.RawQuery)
|
||||
code, err := param()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
code := query.Get("code")
|
||||
if code == "" {
|
||||
return nil, errors.New("no auth code")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, v.Client),
|
||||
request.Timeout,
|
||||
)
|
||||
cctx := context.WithValue(context.Background(), oauth2.HTTPClient, v.Client)
|
||||
ctx, cancel := context.WithTimeout(cctx, request.Timeout)
|
||||
defer cancel()
|
||||
|
||||
token, err := oc.Exchange(ctx, code,
|
||||
oauth2.SetAuthURLParam("code_verifier", cv.CodeChallengePlain()),
|
||||
)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
type emobilityAdapter struct {
|
||||
tr *Identity
|
||||
}
|
||||
|
||||
func (v *emobilityAdapter) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
||||
token, err := v.tr.RefreshToken(nil)
|
||||
if err == nil {
|
||||
token = v.tr.emobilityToken
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return token, err
|
||||
|
||||
v.TokenSource = oc.TokenSource(cctx, token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ type Provider struct {
|
|||
}
|
||||
|
||||
// NewProvider creates a vehicle api provider
|
||||
func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, vin, carModel string, cache time.Duration) *Provider {
|
||||
func NewProvider(log *util.Logger, connect *API, emobility *EmobilityAPI, vin, carModel string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
statusG: provider.Cached(func() (StatusResponse, error) {
|
||||
return api.Status(vin)
|
||||
return connect.Status(vin)
|
||||
}, cache),
|
||||
|
||||
emobilityG: provider.Cached(func() (EmobilityResponse, error) {
|
||||
|
|
@ -31,7 +31,7 @@ func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, vin, carMo
|
|||
}, cache),
|
||||
|
||||
wakeup: func() error {
|
||||
_, _ = api.Status(vin)
|
||||
_, _ = connect.Status(vin)
|
||||
if carModel != "" {
|
||||
_, _ = emobility.Status(vin, carModel)
|
||||
}
|
||||
|
|
@ -39,6 +39,12 @@ func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, vin, carMo
|
|||
},
|
||||
}
|
||||
|
||||
if emobility == nil {
|
||||
impl.emobilityG = func() (EmobilityResponse, error) {
|
||||
return EmobilityResponse{}, api.ErrNotAvailable
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue