Kia/Hyundai: fix login api (#22709)

Signed-off-by: Stefan Rubner <stefan@whocares.de>
This commit is contained in:
stefan 2025-08-03 17:13:22 +02:00 • committed by GitHub
parent 7d3869ace6
commit c73c1d9204
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 76 deletions

View file

@ -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)

View file

@ -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