From 10fb4d8a40ff0d4344e621ef814aae03ed8eac78 Mon Sep 17 00:00:00 2001 From: Dirk-Jan Faber Date: Fri, 8 Aug 2025 10:32:05 +0200 Subject: [PATCH] fix(EU): update KIA/Hyundai authentication for new API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KIA and Hyundai changed their EU authentication system, breaking existing login functionality with 401 Unauthorized errors. This implements the new authentication flow based on the updated API endpoints. Changes: - Update authentication hosts: eu-account.*.com → idpconnect-eu.*.com - Update KIA client ID to new value (fdc85c00-0a2f-4c64-bcb4-2cfb1500730a) - Implement new 3-step OAuth2 flow with connector session keys - Remove language endpoint call that now requires pre-authentication - Update token exchange to use new idpconnect-eu endpoints - Remove unused imports and helper functions The new authentication flow: 1. GET auth URL to extract connector_session_key from redirect 2. POST credentials to new signin endpoint with session key 3. Extract authorization code from redirect location header 4. Exchange code for tokens using new token endpoint Fixes authentication issues for EU region KIA and Hyundai users. Tested with KIA EU account - login, vehicle discovery, and status retrieval all working correctly. --- .../authStrategies/authStrategy.ts | 12 +- .../european.brandAuth.strategy.ts | 217 ++++++++---------- .../european.legacyAuth.strategy.ts | 2 +- src/controllers/european.controller.ts | 31 +-- 4 files changed, 119 insertions(+), 143 deletions(-) diff --git a/src/controllers/authStrategies/authStrategy.ts b/src/controllers/authStrategies/authStrategy.ts index b02ab58..102676a 100644 --- a/src/controllers/authStrategies/authStrategy.ts +++ b/src/controllers/authStrategies/authStrategy.ts @@ -1,6 +1,6 @@ import got from 'got'; import { CookieJar } from 'tough-cookie'; -import { DEFAULT_LANGUAGE, EULanguages, EuropeanBrandEnvironment } from '../../constants/europe'; +import { EuropeanBrandEnvironment } from '../../constants/europe'; export type Code = string; @@ -14,15 +14,11 @@ export interface AuthStrategy { export async function initSession( environment: EuropeanBrandEnvironment, - language: EULanguages = DEFAULT_LANGUAGE, cookies?: CookieJar ): Promise { const cookieJar = cookies ?? new CookieJar(); await got(environment.endpoints.session, { cookieJar }); - await got(environment.endpoints.language, { - method: 'POST', - body: `{"lang":"${language}"}`, - cookieJar, - }); + // Language endpoint now requires authentication, so we skip it + // Language will be set in the authentication URL instead return cookieJar; -} +} \ No newline at end of file diff --git a/src/controllers/authStrategies/european.brandAuth.strategy.ts b/src/controllers/authStrategies/european.brandAuth.strategy.ts index f5ea94b..3227b81 100644 --- a/src/controllers/authStrategies/european.brandAuth.strategy.ts +++ b/src/controllers/authStrategies/european.brandAuth.strategy.ts @@ -2,24 +2,13 @@ import got from 'got'; import { CookieJar } from 'tough-cookie'; import { EULanguages, EuropeanBrandEnvironment } from '../../constants/europe'; import { AuthStrategy, Code, initSession } from './authStrategy'; -import Url, { URLSearchParams } from 'url'; +import { URLSearchParams } from 'url'; const stdHeaders = { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B92 Safari/604.1', }; -const manageGot302 = >( - got: Promise> -): Promise> => { - return got.catch(error => { - if (error.name === 'HTTPError' && error.statusCode === 302) { - return error.response; - } - return Promise.reject(error); - }); -}; - export class EuropeanBrandAuthStrategy implements AuthStrategy { constructor( private readonly environment: EuropeanBrandEnvironment, @@ -30,111 +19,101 @@ export class EuropeanBrandAuthStrategy implements AuthStrategy { return 'EuropeanBrandAuthStrategy'; } - public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code, cookies: CookieJar }> { - const cookieJar = await initSession(this.environment, this.language, options?.cookieJar); - const { body: { userId, serviceId } } = await got(this.environment.endpoints.integration, { - cookieJar, - json: true, - headers: stdHeaders - }); - const brandAuthUrl = this.environment.brandAuthUrl({ language: this.language, userId, serviceId }); - const parsedBrandUrl = Url.parse(brandAuthUrl, true); - const { body: authForm } = await got( - brandAuthUrl, { - cookieJar, - headers: stdHeaders - }); - const actionUrl = /action="([a-z0-9:/\-.?_=&;]*)"/gi.exec(authForm); - const preparedUrl = actionUrl?.[1].replace(/&/g, '&'); - if (!preparedUrl) { - throw new Error('@EuropeanBrandAuthStrategy.login: cannot found the auth url from the form.'); - } - const formData = new URLSearchParams(); - formData.append('username', user.username); - formData.append('password', user.password); - formData.append('credentialId', ''); - formData.append('rememberMe', 'on'); - const { headers: { location: redirectTo }, body: afterAuthForm } = await manageGot302(got.post(preparedUrl, { - cookieJar, - body: formData.toString(), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - ...stdHeaders - }, + public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code, cookies: CookieJar }> { + const cookieJar = await initSession(this.environment, options?.cookieJar); + + // Build the correct auth URL based on the new KIA/Hyundai authentication + const authHost = this.environment.brand === 'kia' + ? 'idpconnect-eu.kia.com' + : 'idpconnect-eu.hyundai.com'; + + const authUrl = `https://${authHost}/auth/api/v2/user/oauth2/authorize?response_type=code&client_id=${this.environment.clientId}&redirect_uri=${this.environment.baseUrl}/api/v1/user/oauth2/redirect&lang=${this.language}&state=ccsp`; + + // Step 1: GET request to auth URL to get connector_session_key + const authResponse = await got(authUrl, { + cookieJar, + headers: stdHeaders, + followRedirect: true, + throwHttpErrors: false, + }); + + // Extract connector_session_key from the final URL after redirects + const urlToCheck = authResponse.url; + + // Try multiple regex patterns to find the session key + let connectorSessionKey: string | null = null; + + // Pattern 1: URL encoded + let match = urlToCheck.match(/connector_session_key%3D([0-9a-fA-F-]{36})/); + if (match) { + connectorSessionKey = match[1]; + } + + // Pattern 2: Not URL encoded + if (!connectorSessionKey) { + match = urlToCheck.match(/connector_session_key=([0-9a-fA-F-]{36})/); + if (match) { + connectorSessionKey = match[1]; + } + } + + if (!connectorSessionKey) { + throw new Error(`@EuropeanBrandAuthStrategy.login: Could not extract connector_session_key from URL: ${urlToCheck}`); + } + + // Step 2: POST to signin endpoint + const signinUrl = `https://${authHost}/auth/account/signin`; + + const formData = new URLSearchParams(); + formData.append('client_id', this.environment.clientId); + formData.append('encryptedPassword', 'false'); + formData.append('orgHmgSid', ''); + formData.append('password', user.password); + formData.append('redirect_uri', `${this.environment.baseUrl}/api/v1/user/oauth2/redirect`); + formData.append('state', 'ccsp'); + formData.append('username', user.username); + formData.append('remember_me', 'false'); + formData.append('connector_session_key', connectorSessionKey); + formData.append('_csrf', ''); + + const signinResponse = await got.post(signinUrl, { + cookieJar, + body: formData.toString(), + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'origin': `https://${authHost}`, + ...stdHeaders + }, followRedirect: false, - })); - if(!redirectTo) { - const errorMessage = /