Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions src/controllers/authStrategies/authStrategy.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -14,15 +14,11 @@ export interface AuthStrategy {

export async function initSession(
environment: EuropeanBrandEnvironment,
language: EULanguages = DEFAULT_LANGUAGE,
cookies?: CookieJar
): Promise<CookieJar> {
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;
}
}
217 changes: 98 additions & 119 deletions src/controllers/authStrategies/european.brandAuth.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T extends Buffer | string | Record<string, unknown>>(
got: Promise<got.Response<T>>
): Promise<got.Response<T>> => {
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,
Expand All @@ -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(/&amp;/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 = /<span class="kc-feedback-text">(.+)<\/span>/gm.exec(afterAuthForm);
if (errorMessage) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Authentication failed with message : ${errorMessage[1]}`);
}
throw new Error('@EuropeanBrandAuthStrategy.login: Authentication failed, cannot retrieve error message');
}
const authResult = await got(redirectTo, {
cookieJar,
headers: stdHeaders
});
let url = authResult.url;
let htmlPage = authResult.body;
if(!url) {
throw new Error(`@EuropeanBrandAuthStrategy.login: after login redirection got stuck : ${htmlPage}`);
}
if(url.includes('login-actions/required-action')) {
const loginActionUrl = /action="([a-z0-9:/\-.?_=&;]*)"/gi.exec(htmlPage);
const loginActionCode = /name="code" value="(.*)"/gi.exec(htmlPage);
if (!loginActionUrl) {
throw new Error('@EuropeanBrandAuthStrategy.login: Cannot find login-actions url.');
}
if (!loginActionCode) {
throw new Error('@EuropeanBrandAuthStrategy.login: Cannot find login-actions code.');
}
const actionUrl = (loginActionUrl[1].startsWith('/')) ? `${parsedBrandUrl.protocol}//${parsedBrandUrl.host}${loginActionUrl[1]}` : loginActionUrl[1];
const loginActionForm = new URLSearchParams();
loginActionForm.append('code', loginActionCode[1]);
loginActionForm.append('accept', '');
const { headers: { location: loginActionRedirect }, body: AfterLoginActionAuthForm } = await manageGot302(got.post(actionUrl, {
cookieJar,
body: loginActionForm.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...stdHeaders
},
}));
if(!loginActionRedirect) {
const errorMessage = /<span class="kc-feedback-text">(.+)<\/span>/gm.exec(AfterLoginActionAuthForm);
if (errorMessage) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Authentication action failed with message : ${errorMessage[1]}`);
}
throw new Error('@EuropeanBrandAuthStrategy.login: Authentication action failed, cannot retrieve error message');
}
const authResult = await got(loginActionRedirect, {
cookieJar,
headers: stdHeaders
});
url = authResult.url;
htmlPage = authResult.body;
}
const { body, statusCode } = await got.post(this.environment.endpoints.silentSignIn, {
cookieJar,
body: {
intUserId: ''
},
json: true,
headers: {
...stdHeaders,
'ccsp-service-id': this.environment.clientId,
}
});
if(!body.redirectUrl) {
throw new Error(`@EuropeanBrandAuthStrategy.login: silent sign In didn't work, could not retrieve auth code. status: ${statusCode}, body: ${JSON.stringify(body)}`);
}
const { code } = Url.parse(body.redirectUrl, true).query;
if (!code) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Cannot find the argument code in ${body.redirectUrl}.`);
}
return {
code: code as Code,
cookies: cookieJar,
};
}
}
throwHttpErrors: false,
});

if (signinResponse.statusCode !== 302) {
throw new Error(`@EuropeanBrandAuthStrategy.login: Signin failed with status ${signinResponse.statusCode}: ${signinResponse.body}`);
}

// Step 3: Extract authorization code from Location header
const location = signinResponse.headers.location;
if (!location) {
throw new Error('@EuropeanBrandAuthStrategy.login: No redirect location found after signin');
}

const codeMatch = location.match(/code=([0-9a-fA-F-]{36}\.[0-9a-fA-F-]{36}\.[0-9a-fA-F-]{36})/);
if (!codeMatch) {
// Try alternative patterns for different code formats
const altMatch = location.match(/code=([^&]+)/);
if (altMatch) {
const code = altMatch[1];
return { code: code as Code, cookies: cookieJar };
}
throw new Error(`@EuropeanBrandAuthStrategy.login: Could not extract authorization code from redirect location: ${location}`);
}

const code = codeMatch[1];

return {
code: code as Code,
cookies: cookieJar,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class EuropeanLegacyAuthStrategy implements AuthStrategy {
user: { username: string; password: string },
options?: { cookieJar: CookieJar }
): Promise<{ code: Code; cookies: CookieJar }> {
const cookieJar = await initSession(this.environment, this.language, options?.cookieJar);
const cookieJar = await initSession(this.environment, options?.cookieJar);
const { body, statusCode } = await got(this.environment.endpoints.login, {
method: 'POST',
json: true,
Expand Down
31 changes: 16 additions & 15 deletions src/controllers/european.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,26 +214,27 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
}
logger.debug('@EuropeController.login: Device registered');

const formData = new URLSearchParams();
formData.append('grant_type', 'authorization_code');
formData.append('redirect_uri', this.environment.endpoints.redirectUri);
formData.append('code', authResult.code);

const response = await got(this.environment.endpoints.token, {
// Updated token exchange to use new endpoint based on Python fix
const tokenUrl = this.environment.brand === 'kia'
? 'https://idpconnect-eu.kia.com/auth/api/v2/user/oauth2/token'
: 'https://idpconnect-eu.hyundai.com/auth/api/v2/user/oauth2/token';

const tokenFormData = new URLSearchParams();
tokenFormData.append('grant_type', 'authorization_code');
tokenFormData.append('code', authResult.code);
tokenFormData.append('redirect_uri', `${this.environment.baseUrl}/api/v1/user/oauth2/redirect`);
tokenFormData.append('client_id', this.environment.clientId);
tokenFormData.append('client_secret', 'secret');

const response = await got(tokenUrl, {
method: 'POST',
headers: {
'Authorization': this.environment.basicToken,
'Content-Type': 'application/x-www-form-urlencoded',
'Host': this.environment.host,
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'User-Agent': 'okhttp/3.10.0',
'grant_type': 'authorization_code',
'ccsp-application-id': this.environment.appId,
'Stamp': await this.environment.stamp(),
},
body: formData.toString(),
body: tokenFormData.toString(),
cookieJar: authResult.cookies,
throwHttpErrors: false,
});

if (response.statusCode !== 200) {
Expand Down Expand Up @@ -355,4 +356,4 @@ export class EuropeanController extends SessionController<EuropeBlueLinkyConfig>
'Content-Type': 'application/json',
};
}
}
}