From c73c1d9204806e0f8b87ed63a50382ee97360247 Mon Sep 17 00:00:00 2001 From: stefan Date: Sun, 3 Aug 2025 17:13:22 +0200 Subject: [PATCH] Kia/Hyundai: fix login api (#22709) Signed-off-by: Stefan Rubner --- vehicle/bluelink.go | 10 +-- vehicle/bluelink/identity.go | 126 +++++++++++++++-------------------- 2 files changed, 60 insertions(+), 76 deletions(-) diff --git a/vehicle/bluelink.go b/vehicle/bluelink.go index ba71c5870..952afe8e1 100644 --- a/vehicle/bluelink.go +++ b/vehicle/bluelink.go @@ -29,10 +29,11 @@ func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) { BasicToken: "NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==", CCSPServiceID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", CCSPApplicationID: bluelink.HyundaiAppID, - AuthClientID: "64621b96-0f0d-11ec-82a8-0242ac130003", - BrandAuthUrl: "https://eu-account.hyundai.com/auth/realms/euhyundaiidm/protocol/openid-connect/auth?client_id=%s&scope=openid+profile+email+phone&response_type=code&hkid_session_reset=true&redirect_uri=%s/api/v1/user/integration/redirect/login&ui_locales=%s&state=%s:%s", + AuthClientID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", + BrandAuthUrl: "%s/auth/api/v2/user/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s/api/v1/user/oauth2/redirect&lang=%s&state=ccsp", PushType: "GCM", Cfb: "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ=", + LoginFormHost: "https://idpconnect-eu.hyundai.com", } return newBluelinkFromConfig("hyundai", other, settings) @@ -45,10 +46,11 @@ func NewKiaFromConfig(other map[string]interface{}) (api.Vehicle, error) { BasicToken: "ZmRjODVjMDAtMGEyZi00YzY0LWJjYjQtMmNmYjE1MDA3MzBhOnNlY3JldA==", CCSPServiceID: "fdc85c00-0a2f-4c64-bcb4-2cfb1500730a", CCSPApplicationID: bluelink.KiaAppID, - AuthClientID: "572e0304-5f8d-4b4c-9dd5-41aa84eed160", - BrandAuthUrl: "https://eu-account.kia.com/auth/realms/eukiaidm/protocol/openid-connect/auth?client_id=%s&scope=openid+profile+email+phone&response_type=code&hkid_session_reset=true&redirect_uri=%s/api/v1/user/integration/redirect/login&ui_locales=%s&state=%s:%s", + AuthClientID: "fdc85c00-0a2f-4c64-bcb4-2cfb1500730a", + BrandAuthUrl: "%s/auth/api/v2/user/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s/api/v1/user/oauth2/redirect&lang=%s&state=ccsp", PushType: "APNS", Cfb: "wLTVxwidmH8CfJYBWSnHD6E0huk0ozdiuygB4hLkM5XCgzAL1Dk5sE36d/bx5PFMbZs=", + LoginFormHost: "https://idpconnect-eu.kia.com", } return newBluelinkFromConfig("kia", other, settings) diff --git a/vehicle/bluelink/identity.go b/vehicle/bluelink/identity.go index 46ee35666..b3c49c1ff 100644 --- a/vehicle/bluelink/identity.go +++ b/vehicle/bluelink/identity.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "github.com/PuerkitoBio/goquery" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" @@ -41,6 +40,7 @@ type Config struct { CCSPApplicationID string PushType string Cfb string + LoginFormHost string } // Identity implements the Kia/Hyundai bluelink identity. @@ -111,6 +111,7 @@ func (v *Identity) getCookies() (cookieClient *request.Helper, err error) { PublicSuffixList: publicsuffix.List, }) + // TODO: check whether &lang= is necessary uri := fmt.Sprintf( "%s/api/v1/user/oauth2/authorize?response_type=code&state=test&client_id=%s&redirect_uri=%s/api/v1/user/oauth2/redirect", v.config.URI, @@ -154,24 +155,24 @@ func (v *Identity) brandLogin(cookieClient *request.Helper, user, password strin err = cookieClient.DoJSON(req, &info) } - var action string var resp *http.Response + // get the connector_session_key + var connectorSessionKey string if err == nil { - uri := fmt.Sprintf(v.config.BrandAuthUrl, v.config.AuthClientID, v.config.URI, "en", info.ServiceId, info.UserId) - + uri := fmt.Sprintf(v.config.BrandAuthUrl, v.config.LoginFormHost, v.config.AuthClientID, v.config.URI, "en") req, err = request.New(http.MethodGet, uri, nil) if err == nil { if resp, err = cookieClient.Do(req); err == nil { defer resp.Body.Close() - - var doc *goquery.Document - if doc, err = goquery.NewDocumentFromReader(resp.Body); err == nil { - err = errors.New("form not found") - - if form := doc.Find("form"); form != nil && form.Length() == 1 { - var ok bool - if action, ok = form.Attr("action"); ok { + // code adapted from hyundai_kia_connect_api + // get redirect URL from request + err = errors.New("connector session key not found") + urlRedirect := resp.Request.URL.Query() + // extract redirect URL + if nextUri := urlRedirect.Get("next_uri"); nextUri != "" { + if nextVal, ok := url.Parse(nextUri); ok == nil { + if connectorSessionKey := nextVal.Query().Get("connector_session_key"); connectorSessionKey != "" { err = nil } } @@ -180,63 +181,44 @@ func (v *Identity) brandLogin(cookieClient *request.Helper, user, password strin } } - if err == nil { - data := url.Values{ - "username": {user}, - "password": {password}, - "credentialId": {""}, - "rememberMe": {"on"}, - } - - req, err = request.New(http.MethodPost, action, strings.NewReader(data.Encode()), request.URLEncoding) - if err == nil { - cookieClient.CheckRedirect = request.DontFollow - if resp, err = cookieClient.Do(req); err == nil { - defer resp.Body.Close() - - // need 302 - if resp.StatusCode != http.StatusFound { - err = errors.New("missing redirect") - - if doc, err2 := goquery.NewDocumentFromReader(resp.Body); err2 == nil { - if span := doc.Find("span[class=kc-feedback-text]"); span != nil && span.Length() == 1 { - err = errors.New(span.Text()) - } - } - } - } - - cookieClient.CheckRedirect = nil - } - } - - if err == nil { - resp, err = cookieClient.Get(resp.Header.Get("Location")) - if err == nil { - defer resp.Body.Close() - } - } - + // if we have the connectorSessionKey, go on and find the login code var code string if err == nil { - data := map[string]string{ - "intUserId": "", + // build new request uri + uri := fmt.Sprintf("%s%s", v.config.LoginFormHost, "/auth/account/signin") + data := url.Values{ + "client_id": {v.config.CCSPServiceID}, + "encryptedPassword": {"false"}, + "orgHmgSid": {""}, + "password": {password}, + "redirect_uri": {v.config.URI + "/api/v1/user/oauth2/redirect"}, + "state": {"ccsp"}, + "username": {user}, + "remember_me": {"false"}, + "connector_session_key": {connectorSessionKey}, + "_csrf": {""}, } - req, err = request.New(http.MethodPost, v.config.URI+SilentSigninURL, request.MarshalJSON(data), request.JSONEncoding) + // create a client that doesn't honor redirects so we receive the original response + // no idea how to do that with the internal request.New(...) function + sc := http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + req, err = http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode())) if err == nil { - req.Header.Set("ccsp-service-id", v.config.CCSPServiceID) - cookieClient.CheckRedirect = request.DontFollow + req.PostForm = data + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Origin", v.config.LoginFormHost) - var res struct { - RedirectUrl string `json:"redirectUrl"` - } - - if err = cookieClient.DoJSON(req, &res); err == nil { - var uri *url.URL - if uri, err = url.Parse(res.RedirectUrl); err == nil { - if code = uri.Query().Get("code"); len(code) == 0 { - err = errors.New("code not found") + if resp, err = sc.Do(req); err == nil { + location := resp.Header.Get("Location") + err = errors.New("code location not found") + if locationUrl, ok := url.Parse(location); ok == nil { + if code = locationUrl.Query().Get("code"); code != "" { + err = nil } } } @@ -276,21 +258,22 @@ func (v *Identity) bluelinkLogin(cookieClient *request.Helper, user, password st } func (v *Identity) exchangeCode(accCode string) (*oauth2.Token, error) { + uri := v.config.LoginFormHost + "/auth/api/v2/user/oauth2/token" headers := map[string]string{ - "Authorization": "Basic " + v.config.BasicToken, - "Content-type": "application/x-www-form-urlencoded", - "User-Agent": "okhttp/3.10.0", + "Content-type": "application/x-www-form-urlencoded", + "User-Agent": "okhttp/3.10.0", } - data := url.Values{ - "grant_type": {"authorization_code"}, - "redirect_uri": {v.config.URI + "/api/v1/user/oauth2/redirect"}, - "code": {accCode}, + "grant_type": {"authorization_code"}, + "code": {accCode}, + "redirect_uri": {v.config.URI + "/api/v1/user/oauth2/redirect"}, + "client_id": {v.config.CCSPServiceID}, + "client_secret": {"secret"}, } var token oauth2.Token - req, _ := request.New(http.MethodPost, v.config.URI+TokenURL, strings.NewReader(data.Encode()), headers) + req, _ := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers) err := v.DoJSON(req, &token) return util.TokenWithExpiry(&token), err @@ -324,7 +307,6 @@ func (v *Identity) Login(user, password, language string) (err error) { if user == "" || password == "" { return api.ErrMissingCredentials } - v.deviceID, err = v.getDeviceID() var cookieClient *request.Helper