Skip to content

Commit f45ed36

Browse files
authored
fix(EU): update KIA/Hyundai authentication for new API endpoints (#300)
1 parent 4b89157 commit f45ed36

File tree

4 files changed

+119
-143
lines changed

4 files changed

+119
-143
lines changed
Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import got from 'got';
22
import { CookieJar } from 'tough-cookie';
3-
import { DEFAULT_LANGUAGE, EULanguages, EuropeanBrandEnvironment } from '../../constants/europe';
3+
import { EuropeanBrandEnvironment } from '../../constants/europe';
44

55
export type Code = string;
66

@@ -14,15 +14,11 @@ export interface AuthStrategy {
1414

1515
export async function initSession(
1616
environment: EuropeanBrandEnvironment,
17-
language: EULanguages = DEFAULT_LANGUAGE,
1817
cookies?: CookieJar
1918
): Promise<CookieJar> {
2019
const cookieJar = cookies ?? new CookieJar();
2120
await got(environment.endpoints.session, { cookieJar });
22-
await got(environment.endpoints.language, {
23-
method: 'POST',
24-
body: `{"lang":"${language}"}`,
25-
cookieJar,
26-
});
21+
// Language endpoint now requires authentication, so we skip it
22+
// Language will be set in the authentication URL instead
2723
return cookieJar;
28-
}
24+
}

src/controllers/authStrategies/european.brandAuth.strategy.ts

Lines changed: 98 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,13 @@ import got from 'got';
22
import { CookieJar } from 'tough-cookie';
33
import { EULanguages, EuropeanBrandEnvironment } from '../../constants/europe';
44
import { AuthStrategy, Code, initSession } from './authStrategy';
5-
import Url, { URLSearchParams } from 'url';
5+
import { URLSearchParams } from 'url';
66

77
const stdHeaders = {
88
'User-Agent':
99
'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',
1010
};
1111

12-
const manageGot302 = <T extends Buffer | string | Record<string, unknown>>(
13-
got: Promise<got.Response<T>>
14-
): Promise<got.Response<T>> => {
15-
return got.catch(error => {
16-
if (error.name === 'HTTPError' && error.statusCode === 302) {
17-
return error.response;
18-
}
19-
return Promise.reject(error);
20-
});
21-
};
22-
2312
export class EuropeanBrandAuthStrategy implements AuthStrategy {
2413
constructor(
2514
private readonly environment: EuropeanBrandEnvironment,
@@ -30,111 +19,101 @@ export class EuropeanBrandAuthStrategy implements AuthStrategy {
3019
return 'EuropeanBrandAuthStrategy';
3120
}
3221

33-
public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code, cookies: CookieJar }> {
34-
const cookieJar = await initSession(this.environment, this.language, options?.cookieJar);
35-
const { body: { userId, serviceId } } = await got(this.environment.endpoints.integration, {
36-
cookieJar,
37-
json: true,
38-
headers: stdHeaders
39-
});
40-
const brandAuthUrl = this.environment.brandAuthUrl({ language: this.language, userId, serviceId });
41-
const parsedBrandUrl = Url.parse(brandAuthUrl, true);
42-
const { body: authForm } = await got(
43-
brandAuthUrl, {
44-
cookieJar,
45-
headers: stdHeaders
46-
});
47-
const actionUrl = /action="([a-z0-9:/\-.?_=&;]*)"/gi.exec(authForm);
48-
const preparedUrl = actionUrl?.[1].replace(/&amp;/g, '&');
49-
if (!preparedUrl) {
50-
throw new Error('@EuropeanBrandAuthStrategy.login: cannot found the auth url from the form.');
51-
}
52-
const formData = new URLSearchParams();
53-
formData.append('username', user.username);
54-
formData.append('password', user.password);
55-
formData.append('credentialId', '');
56-
formData.append('rememberMe', 'on');
57-
const { headers: { location: redirectTo }, body: afterAuthForm } = await manageGot302(got.post(preparedUrl, {
58-
cookieJar,
59-
body: formData.toString(),
60-
headers: {
61-
'Content-Type': 'application/x-www-form-urlencoded',
62-
...stdHeaders
63-
},
22+
public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code, cookies: CookieJar }> {
23+
const cookieJar = await initSession(this.environment, options?.cookieJar);
24+
25+
// Build the correct auth URL based on the new KIA/Hyundai authentication
26+
const authHost = this.environment.brand === 'kia'
27+
? 'idpconnect-eu.kia.com'
28+
: 'idpconnect-eu.hyundai.com';
29+
30+
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`;
31+
32+
// Step 1: GET request to auth URL to get connector_session_key
33+
const authResponse = await got(authUrl, {
34+
cookieJar,
35+
headers: stdHeaders,
36+
followRedirect: true,
37+
throwHttpErrors: false,
38+
});
39+
40+
// Extract connector_session_key from the final URL after redirects
41+
const urlToCheck = authResponse.url;
42+
43+
// Try multiple regex patterns to find the session key
44+
let connectorSessionKey: string | null = null;
45+
46+
// Pattern 1: URL encoded
47+
let match = urlToCheck.match(/connector_session_key%3D([0-9a-fA-F-]{36})/);
48+
if (match) {
49+
connectorSessionKey = match[1];
50+
}
51+
52+
// Pattern 2: Not URL encoded
53+
if (!connectorSessionKey) {
54+
match = urlToCheck.match(/connector_session_key=([0-9a-fA-F-]{36})/);
55+
if (match) {
56+
connectorSessionKey = match[1];
57+
}
58+
}
59+
60+
if (!connectorSessionKey) {
61+
throw new Error(`@EuropeanBrandAuthStrategy.login: Could not extract connector_session_key from URL: ${urlToCheck}`);
62+
}
63+
64+
// Step 2: POST to signin endpoint
65+
const signinUrl = `https://${authHost}/auth/account/signin`;
66+
67+
const formData = new URLSearchParams();
68+
formData.append('client_id', this.environment.clientId);
69+
formData.append('encryptedPassword', 'false');
70+
formData.append('orgHmgSid', '');
71+
formData.append('password', user.password);
72+
formData.append('redirect_uri', `${this.environment.baseUrl}/api/v1/user/oauth2/redirect`);
73+
formData.append('state', 'ccsp');
74+
formData.append('username', user.username);
75+
formData.append('remember_me', 'false');
76+
formData.append('connector_session_key', connectorSessionKey);
77+
formData.append('_csrf', '');
78+
79+
const signinResponse = await got.post(signinUrl, {
80+
cookieJar,
81+
body: formData.toString(),
82+
headers: {
83+
'content-type': 'application/x-www-form-urlencoded',
84+
'origin': `https://${authHost}`,
85+
...stdHeaders
86+
},
6487
followRedirect: false,
65-
}));
66-
if(!redirectTo) {
67-
const errorMessage = /<span class="kc-feedback-text">(.+)<\/span>/gm.exec(afterAuthForm);
68-
if (errorMessage) {
69-
throw new Error(`@EuropeanBrandAuthStrategy.login: Authentication failed with message : ${errorMessage[1]}`);
70-
}
71-
throw new Error('@EuropeanBrandAuthStrategy.login: Authentication failed, cannot retrieve error message');
72-
}
73-
const authResult = await got(redirectTo, {
74-
cookieJar,
75-
headers: stdHeaders
76-
});
77-
let url = authResult.url;
78-
let htmlPage = authResult.body;
79-
if(!url) {
80-
throw new Error(`@EuropeanBrandAuthStrategy.login: after login redirection got stuck : ${htmlPage}`);
81-
}
82-
if(url.includes('login-actions/required-action')) {
83-
const loginActionUrl = /action="([a-z0-9:/\-.?_=&;]*)"/gi.exec(htmlPage);
84-
const loginActionCode = /name="code" value="(.*)"/gi.exec(htmlPage);
85-
if (!loginActionUrl) {
86-
throw new Error('@EuropeanBrandAuthStrategy.login: Cannot find login-actions url.');
87-
}
88-
if (!loginActionCode) {
89-
throw new Error('@EuropeanBrandAuthStrategy.login: Cannot find login-actions code.');
90-
}
91-
const actionUrl = (loginActionUrl[1].startsWith('/')) ? `${parsedBrandUrl.protocol}//${parsedBrandUrl.host}${loginActionUrl[1]}` : loginActionUrl[1];
92-
const loginActionForm = new URLSearchParams();
93-
loginActionForm.append('code', loginActionCode[1]);
94-
loginActionForm.append('accept', '');
95-
const { headers: { location: loginActionRedirect }, body: AfterLoginActionAuthForm } = await manageGot302(got.post(actionUrl, {
96-
cookieJar,
97-
body: loginActionForm.toString(),
98-
headers: {
99-
'Content-Type': 'application/x-www-form-urlencoded',
100-
...stdHeaders
101-
},
102-
}));
103-
if(!loginActionRedirect) {
104-
const errorMessage = /<span class="kc-feedback-text">(.+)<\/span>/gm.exec(AfterLoginActionAuthForm);
105-
if (errorMessage) {
106-
throw new Error(`@EuropeanBrandAuthStrategy.login: Authentication action failed with message : ${errorMessage[1]}`);
107-
}
108-
throw new Error('@EuropeanBrandAuthStrategy.login: Authentication action failed, cannot retrieve error message');
109-
}
110-
const authResult = await got(loginActionRedirect, {
111-
cookieJar,
112-
headers: stdHeaders
113-
});
114-
url = authResult.url;
115-
htmlPage = authResult.body;
116-
}
117-
const { body, statusCode } = await got.post(this.environment.endpoints.silentSignIn, {
118-
cookieJar,
119-
body: {
120-
intUserId: ''
121-
},
122-
json: true,
123-
headers: {
124-
...stdHeaders,
125-
'ccsp-service-id': this.environment.clientId,
126-
}
127-
});
128-
if(!body.redirectUrl) {
129-
throw new Error(`@EuropeanBrandAuthStrategy.login: silent sign In didn't work, could not retrieve auth code. status: ${statusCode}, body: ${JSON.stringify(body)}`);
130-
}
131-
const { code } = Url.parse(body.redirectUrl, true).query;
132-
if (!code) {
133-
throw new Error(`@EuropeanBrandAuthStrategy.login: Cannot find the argument code in ${body.redirectUrl}.`);
134-
}
135-
return {
136-
code: code as Code,
137-
cookies: cookieJar,
138-
};
139-
}
140-
}
88+
throwHttpErrors: false,
89+
});
90+
91+
if (signinResponse.statusCode !== 302) {
92+
throw new Error(`@EuropeanBrandAuthStrategy.login: Signin failed with status ${signinResponse.statusCode}: ${signinResponse.body}`);
93+
}
94+
95+
// Step 3: Extract authorization code from Location header
96+
const location = signinResponse.headers.location;
97+
if (!location) {
98+
throw new Error('@EuropeanBrandAuthStrategy.login: No redirect location found after signin');
99+
}
100+
101+
const codeMatch = location.match(/code=([0-9a-fA-F-]{36}\.[0-9a-fA-F-]{36}\.[0-9a-fA-F-]{36})/);
102+
if (!codeMatch) {
103+
// Try alternative patterns for different code formats
104+
const altMatch = location.match(/code=([^&]+)/);
105+
if (altMatch) {
106+
const code = altMatch[1];
107+
return { code: code as Code, cookies: cookieJar };
108+
}
109+
throw new Error(`@EuropeanBrandAuthStrategy.login: Could not extract authorization code from redirect location: ${location}`);
110+
}
111+
112+
const code = codeMatch[1];
113+
114+
return {
115+
code: code as Code,
116+
cookies: cookieJar,
117+
};
118+
}
119+
}

src/controllers/authStrategies/european.legacyAuth.strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class EuropeanLegacyAuthStrategy implements AuthStrategy {
1818
user: { username: string; password: string },
1919
options?: { cookieJar: CookieJar }
2020
): Promise<{ code: Code; cookies: CookieJar }> {
21-
const cookieJar = await initSession(this.environment, this.language, options?.cookieJar);
21+
const cookieJar = await initSession(this.environment, options?.cookieJar);
2222
const { body, statusCode } = await got(this.environment.endpoints.login, {
2323
method: 'POST',
2424
json: true,

src/controllers/european.controller.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -214,26 +214,27 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
214214
}
215215
logger.debug('@EuropeController.login: Device registered');
216216

217-
const formData = new URLSearchParams();
218-
formData.append('grant_type', 'authorization_code');
219-
formData.append('redirect_uri', this.environment.endpoints.redirectUri);
220-
formData.append('code', authResult.code);
221-
222-
const response = await got(this.environment.endpoints.token, {
217+
// Updated token exchange to use new endpoint based on Python fix
218+
const tokenUrl = this.environment.brand === 'kia'
219+
? 'https://idpconnect-eu.kia.com/auth/api/v2/user/oauth2/token'
220+
: 'https://idpconnect-eu.hyundai.com/auth/api/v2/user/oauth2/token';
221+
222+
const tokenFormData = new URLSearchParams();
223+
tokenFormData.append('grant_type', 'authorization_code');
224+
tokenFormData.append('code', authResult.code);
225+
tokenFormData.append('redirect_uri', `${this.environment.baseUrl}/api/v1/user/oauth2/redirect`);
226+
tokenFormData.append('client_id', this.environment.clientId);
227+
tokenFormData.append('client_secret', 'secret');
228+
229+
const response = await got(tokenUrl, {
223230
method: 'POST',
224231
headers: {
225-
'Authorization': this.environment.basicToken,
226232
'Content-Type': 'application/x-www-form-urlencoded',
227-
'Host': this.environment.host,
228-
'Connection': 'Keep-Alive',
229-
'Accept-Encoding': 'gzip',
230233
'User-Agent': 'okhttp/3.10.0',
231-
'grant_type': 'authorization_code',
232-
'ccsp-application-id': this.environment.appId,
233-
'Stamp': await this.environment.stamp(),
234234
},
235-
body: formData.toString(),
235+
body: tokenFormData.toString(),
236236
cookieJar: authResult.cookies,
237+
throwHttpErrors: false,
237238
});
238239

239240
if (response.statusCode !== 200) {
@@ -355,4 +356,4 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
355356
'Content-Type': 'application/json',
356357
};
357358
}
358-
}
359+
}

0 commit comments

Comments
 (0)