From 76a34cf8ae7f9a38570fd87ee3bef0eb2442ec55 Mon Sep 17 00:00:00 2001 From: Klaus Jaroslawsky Date: Mon, 8 Dec 2025 11:51:40 +0100 Subject: [PATCH] European AuthStrategy fixed. evcc code used as template. --- package-lock.json | 50 ++++- package.json | 1 + src/constants/europe.ts | 179 ++++++++++-------- .../authStrategies/authStrategy.ts | 10 +- .../european.brandAuth.strategy.ts | 109 +++-------- .../european.legacyAuth.strategy.ts | 2 +- src/controllers/chinese.controller.ts | 4 +- src/controllers/european.controller.ts | 152 ++++++--------- 8 files changed, 243 insertions(+), 264 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca4c3c9..776534e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "10.0.0", "license": "MIT", "dependencies": { + "base64-js": "^1.5.1", "got": "^9.6.0", "push-receiver": "^2.1.1", "tough-cookie": "^4.0.0", @@ -93,6 +94,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", @@ -3803,6 +3805,7 @@ "version": "20.11.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3901,6 +3904,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.4.0", "@typescript-eslint/types": "7.4.0", @@ -4116,6 +4120,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4483,6 +4488,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -4537,6 +4562,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -5323,6 +5349,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6853,6 +6880,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9818,6 +9846,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -10436,6 +10465,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10554,6 +10584,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10976,6 +11007,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", @@ -13533,6 +13565,7 @@ "version": "20.11.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "peer": true, "requires": { "undici-types": "~5.26.4" } @@ -13611,6 +13644,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "7.4.0", "@typescript-eslint/types": "7.4.0", @@ -13738,7 +13772,8 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -14018,6 +14053,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -14055,6 +14095,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -14658,6 +14699,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -15787,6 +15829,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -17987,6 +18030,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", "dev": true, + "peer": true, "requires": { "@rollup/rollup-android-arm-eabi": "4.13.2", "@rollup/rollup-android-arm64": "4.13.2", @@ -18428,6 +18472,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -18499,7 +18544,8 @@ "version": "5.4.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", - "dev": true + "dev": true, + "peer": true }, "undici": { "version": "6.10.2", diff --git a/package.json b/package.json index 968b143..9531f12 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "author": "Hacksore", "license": "MIT", "dependencies": { + "base64-js": "^1.5.1", "got": "^9.6.0", "push-receiver": "^2.1.1", "tough-cookie": "^4.0.0", diff --git a/src/constants/europe.ts b/src/constants/europe.ts index fdcdc17..5fce2b1 100644 --- a/src/constants/europe.ts +++ b/src/constants/europe.ts @@ -1,7 +1,6 @@ -import { REGIONS } from '../constants'; import { EuropeBlueLinkyConfig } from '../controllers/european.controller'; import { Brand } from '../interfaces/common.interfaces'; -import { StampMode, getStampGenerator } from './stamps'; +import * as base64 from 'base64-js'; export type EULanguages = | 'cs' @@ -40,115 +39,135 @@ export interface EuropeanBrandEnvironment { brand: Brand; host: string; baseUrl: string; - clientId: string; - appId: string; + + ccspServiceID: string; + ccspServiceSecret: string; + ccspApplicationID: string; + cfb: string; + basicToken: string; + pushType: string; + loginFormHost: string; + endpoints: { - integration: string; - silentSignIn: string; - session: string; - login: string; - language: string; - redirectUri: string; - token: string; + deviceIdURL: string; + integrationInfoURL: string; + silentSigninURL: string; + languageURL: string; + loginURL: string; + tokenURL: string; }; - basicToken: string; - GCMSenderID: string; - stamp: () => Promise; - brandAuthUrl: (options: { language: EULanguages; serviceId: string; userId: string }) => string; + stamp: { result: string | null; error: Error | null }; } const getEndpoints = ( - baseUrl: string, - clientId: string + baseUrl: string ): EuropeanBrandEnvironment['endpoints'] => ({ - session: `${baseUrl}/api/v1/user/oauth2/authorize?response_type=code&state=test&client_id=${clientId}&redirect_uri=${baseUrl}/api/v1/user/oauth2/redirect`, - login: `${baseUrl}/api/v1/user/signin`, - language: `${baseUrl}/api/v1/user/language`, - redirectUri: `${baseUrl}/api/v1/user/oauth2/redirect`, - token: `${baseUrl}/api/v1/user/oauth2/token`, - integration: `${baseUrl}/api/v1/user/integrationinfo`, - silentSignIn: `${baseUrl}/api/v1/user/silentsignin`, + deviceIdURL: `${baseUrl}/api/v1/spa/notifications/register`, + integrationInfoURL: `${baseUrl}/api/v1/user/integrationinfo`, + silentSigninURL: `${baseUrl}/api/v1/user/silentsignin`, + languageURL: `${baseUrl}/api/v1/user/language`, + loginURL: `${baseUrl}/api/v1/user/signin`, + tokenURL: '/auth/api/v2/user/oauth2/token', }); -type EnvironmentConfig = Required> & - Partial>; -type BrandEnvironmentConfig = Pick & Partial; +const getStamp = (cfb: string, ccspApplicationID: string): { result: string | null; error: Error | null } => { + try { + const cfb64 = base64.toByteArray(cfb); + + const timestamp = Math.floor(Date.now()).toString(); + const raw = ccspApplicationID + ':' + timestamp; + const rawBytes = new TextEncoder().encode(raw); + + if (cfb64.length !== rawBytes.length) { + return { + result: null, + error: new Error(`cfb and raw length not equal: ${cfb64.length} != ${rawBytes.length}`) + }; + } + + const enc = new Uint8Array(cfb64.length); + for (let i = 0; i < cfb64.length; i++) { + enc[i] = cfb64[i] ^ rawBytes[i]; + } + + return { + result: base64.fromByteArray(enc), + error: null + }; + } catch (err) { + return { + result: null, + error: err as Error + }; + } +}; + +type BrandEnvironmentConfig = Pick; + +const getHyundaiEnvironment = (): EuropeanBrandEnvironment => { + const host = 'prd.eu-ccapi.hyundai.com:8080'; + const baseUrl = `https://${host}`; + + const ccspServiceID = '6d477c38-3ca4-4cf3-9557-2a1929a94654'; + const ccspServiceSecret = 'KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV'; + const ccspApplicationID = '014d2225-8495-4735-812d-2616334fd15d'; + const cfb = 'RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ='; + const basicToken = 'NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg=='; + const pushType = 'GCM'; + const loginFormHost = 'https://idpconnect-eu.hyundai.com'; -const getHyundaiEnvironment = ({ - stampMode, - stampsFile, -}: EnvironmentConfig): EuropeanBrandEnvironment => { - const host = 'prd.eu-ccapi.hyundai.com:8080'; - const baseUrl = `https://${host}`; - const clientId = '6d477c38-3ca4-4cf3-9557-2a1929a94654'; - const appId = '1eba27d2-9a5b-4eba-8ec7-97eb6c62fb51'; return { brand: 'hyundai', host, baseUrl, - clientId, - appId, - endpoints: Object.freeze(getEndpoints(baseUrl, clientId)), - basicToken: - 'Basic NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==', - GCMSenderID: '414998006775', - stamp: getStampGenerator({ - appId, - brand: 'hyundai', - mode: stampMode, - region: REGIONS.EU, - stampHost: 'https://raw.githubusercontent.com/neoPix/bluelinky-stamps/master/', - stampsFile: stampsFile, - }), - brandAuthUrl({ language, serviceId, userId }) { - const newAuthClientId = '64621b96-0f0d-11ec-82a8-0242ac130003'; - return `https://eu-account.hyundai.com/auth/realms/euhyundaiidm/protocol/openid-connect/auth?client_id=${newAuthClientId}&scope=openid%20profile%20email%20phone&response_type=code&hkid_session_reset=true&redirect_uri=${baseUrl}/api/v1/user/integration/redirect/login&ui_locales=${language}&state=${serviceId}:${userId}`; - }, + ccspServiceID, + ccspServiceSecret, + ccspApplicationID, + cfb, + basicToken, + pushType, + loginFormHost, + endpoints: Object.freeze(getEndpoints(baseUrl)), + stamp: getStamp(cfb, ccspApplicationID), }; }; -const getKiaEnvironment = ({ - stampMode, - stampsFile, -}: EnvironmentConfig): EuropeanBrandEnvironment => { +const getKiaEnvironment = (): EuropeanBrandEnvironment => { const host = 'prd.eu-ccapi.kia.com:8080'; const baseUrl = `https://${host}`; - const clientId = 'fdc85c00-0a2f-4c64-bcb4-2cfb1500730a'; - const appId = 'a2b8469b-30a3-4361-8e13-6fceea8fbe74'; + + const ccspServiceID = 'fdc85c00-0a2f-4c64-bcb4-2cfb1500730a'; + const ccspServiceSecret = 'secret'; + const ccspApplicationID = 'a2b8469b-30a3-4361-8e13-6fceea8fbe74'; + const cfb = 'wLTVxwidmH8CfJYBWSnHD6E0huk0ozdiuygB4hLkM5XCgzAL1Dk5sE36d/bx5PFMbZs='; + const basicToken = 'ZmRjODVjMDAtMGEyZi00YzY0LWJjYjQtMmNmYjE1MDA3MzBhOnNlY3JldA=='; + const pushType = 'APNS'; + const loginFormHost = 'https://idpconnect-eu.kia.com'; + return { brand: 'kia', host, baseUrl, - clientId, - appId, - endpoints: Object.freeze(getEndpoints(baseUrl, clientId)), - basicToken: 'Basic ZmRjODVjMDAtMGEyZi00YzY0LWJjYjQtMmNmYjE1MDA3MzBhOnNlY3JldA==', - GCMSenderID: '345127537656', - stamp: getStampGenerator({ - appId, - brand: 'kia', - mode: stampMode, - region: REGIONS.EU, - stampHost: 'https://raw.githubusercontent.com/neoPix/bluelinky-stamps/master/', - stampsFile: stampsFile, - }), - brandAuthUrl({ language, serviceId, userId }) { - const newAuthClientId = '572e0304-5f8d-4b4c-9dd5-41aa84eed160'; - return `https://eu-account.kia.com/auth/realms/eukiaidm/protocol/openid-connect/auth?client_id=${newAuthClientId}&scope=openid%20profile%20email%20phone&response_type=code&hkid_session_reset=true&redirect_uri=${baseUrl}/api/v1/user/integration/redirect/login&ui_locales=${language}&state=${serviceId}:${userId}`; - }, + ccspServiceID, + ccspServiceSecret, + ccspApplicationID, + cfb, + basicToken, + pushType, + loginFormHost, + endpoints: Object.freeze(getEndpoints(baseUrl)), + stamp: getStamp(cfb, ccspApplicationID), }; }; export const getBrandEnvironment = ({ brand, - stampMode = StampMode.DISTANT, - stampsFile, }: BrandEnvironmentConfig): EuropeanBrandEnvironment => { switch (brand) { case 'hyundai': - return Object.freeze(getHyundaiEnvironment({ stampMode, stampsFile })); + return Object.freeze(getHyundaiEnvironment()); case 'kia': - return Object.freeze(getKiaEnvironment({ stampMode, stampsFile })); + return Object.freeze(getKiaEnvironment()); default: throw new Error(`Constructor ${brand} is not managed.`); } diff --git a/src/controllers/authStrategies/authStrategy.ts b/src/controllers/authStrategies/authStrategy.ts index 102676a..52342fd 100644 --- a/src/controllers/authStrategies/authStrategy.ts +++ b/src/controllers/authStrategies/authStrategy.ts @@ -1,15 +1,19 @@ -import got from 'got'; import { CookieJar } from 'tough-cookie'; import { EuropeanBrandEnvironment } from '../../constants/europe'; export type Code = string; +export type Token = { + refresh_token: string; + access_token: string; + expires_in: number; +}; export interface AuthStrategy { readonly name: string; login( user: { username: string; password: string }, options?: { cookieJar?: CookieJar } - ): Promise<{ code: Code; cookies: CookieJar }>; + ): Promise<{ code: Code | Token; cookies: CookieJar }>; } export async function initSession( @@ -17,7 +21,7 @@ export async function initSession( cookies?: CookieJar ): Promise { const cookieJar = cookies ?? new CookieJar(); - await got(environment.endpoints.session, { cookieJar }); + //!!!// await got(environment.endpoints.session, { cookieJar }); // Language endpoint now requires authentication, so we skip it // Language will be set in the authentication URL instead return cookieJar; diff --git a/src/controllers/authStrategies/european.brandAuth.strategy.ts b/src/controllers/authStrategies/european.brandAuth.strategy.ts index 3227b81..62e5972 100644 --- a/src/controllers/authStrategies/european.brandAuth.strategy.ts +++ b/src/controllers/authStrategies/european.brandAuth.strategy.ts @@ -1,14 +1,9 @@ import got from 'got'; import { CookieJar } from 'tough-cookie'; import { EULanguages, EuropeanBrandEnvironment } from '../../constants/europe'; -import { AuthStrategy, Code, initSession } from './authStrategy'; +import { AuthStrategy, Code, Token, initSession } from './authStrategy'; 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', -}; - export class EuropeanBrandAuthStrategy implements AuthStrategy { constructor( private readonly environment: EuropeanBrandEnvironment, @@ -19,100 +14,42 @@ export class EuropeanBrandAuthStrategy implements AuthStrategy { return 'EuropeanBrandAuthStrategy'; } - public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code, cookies: CookieJar }> { + public async login(user: { username: string; password: string; }, options?: { cookieJar?: CookieJar }): Promise<{ code: Code | Token, 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]; - } - } + const uri = this.environment.loginFormHost + this.environment.endpoints.tokenURL; - 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 headers = { + 'Content-type': 'application/x-www-form-urlencoded', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19_CCS_APP_AOS', + }; 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, + formData.append('grant_type', 'refresh_token'); + formData.append('refresh_token', user.password); + formData.append('client_id', this.environment.ccspServiceID); + formData.append('client_secret', this.environment.ccspServiceSecret); + + const authResponse = await got.post(uri, { +// cookieJar, + headers: headers, body: formData.toString(), - headers: { - 'content-type': 'application/x-www-form-urlencoded', - 'origin': `https://${authHost}`, - ...stdHeaders - }, - followRedirect: false, + followRedirect: true, throwHttpErrors: false, }); - if (signinResponse.statusCode !== 302) { - throw new Error(`@EuropeanBrandAuthStrategy.login: Signin failed with status ${signinResponse.statusCode}: ${signinResponse.body}`); + if (authResponse.statusCode !== 200) { + throw new Error(`@EuropeanBrandAuthStrategy.login: Could not get access_token from URL: ${uri}`); } - // 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 token = JSON.parse(authResponse.body) as Token; + if (! token.refresh_token && user.password != '') { + token.refresh_token = user.password; } - 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]; - + //const code = ''; return { - code: code as Code, + code: token, cookies: cookieJar, }; } diff --git a/src/controllers/authStrategies/european.legacyAuth.strategy.ts b/src/controllers/authStrategies/european.legacyAuth.strategy.ts index ff21a53..6216338 100644 --- a/src/controllers/authStrategies/european.legacyAuth.strategy.ts +++ b/src/controllers/authStrategies/european.legacyAuth.strategy.ts @@ -19,7 +19,7 @@ export class EuropeanLegacyAuthStrategy implements AuthStrategy { options?: { cookieJar: CookieJar } ): Promise<{ code: Code; cookies: CookieJar }> { const cookieJar = await initSession(this.environment, options?.cookieJar); - const { body, statusCode } = await got(this.environment.endpoints.login, { + const { body, statusCode } = await got(this.environment.endpoints.loginURL, { method: 'POST', json: true, body: { diff --git a/src/controllers/chinese.controller.ts b/src/controllers/chinese.controller.ts index f3e1627..3775be7 100644 --- a/src/controllers/chinese.controller.ts +++ b/src/controllers/chinese.controller.ts @@ -14,7 +14,7 @@ import { URLSearchParams } from 'url'; import { CookieJar } from 'tough-cookie'; import { VehicleRegisterOptions } from '../interfaces/common.interfaces'; import { asyncMap, manageBluelinkyError, Stringifiable, uuidV4 } from '../tools/common.tools'; -import { AuthStrategy, Code } from './authStrategies/authStrategy'; +import { Code } from './authStrategies/authStrategy'; import { ChineseLegacyAuthStrategy } from './authStrategies/chinese.legacyAuth.strategy'; export interface ChineseBlueLinkConfig extends BlueLinkyConfig { @@ -31,7 +31,7 @@ interface ChineseVehicleDescription { export class ChineseController extends SessionController { private _environment: ChineseBrandEnvironment; private authStrategies: { - main: AuthStrategy; + main: ChineseLegacyAuthStrategy; }; constructor(userConfig: ChineseBlueLinkConfig) { super(userConfig); diff --git a/src/controllers/european.controller.ts b/src/controllers/european.controller.ts index d8a0838..f6a2997 100644 --- a/src/controllers/european.controller.ts +++ b/src/controllers/european.controller.ts @@ -17,16 +17,13 @@ import { URLSearchParams } from 'url'; import { CookieJar } from 'tough-cookie'; import { VehicleRegisterOptions } from '../interfaces/common.interfaces'; import { asyncMap, manageBluelinkyError, Stringifiable, uuidV4 } from '../tools/common.tools'; -import { AuthStrategy, Code } from './authStrategies/authStrategy'; +import { AuthStrategy, Code, Token } from './authStrategies/authStrategy'; import { EuropeanBrandAuthStrategy } from './authStrategies/european.brandAuth.strategy'; import { EuropeanLegacyAuthStrategy } from './authStrategies/european.legacyAuth.strategy'; -import { StampMode } from '../constants/stamps'; export interface EuropeBlueLinkyConfig extends BlueLinkyConfig { language?: EULanguages; region: 'EU'; - stampMode?: StampMode; - stampsFile?: string; } interface EuropeanVehicleDescription { @@ -67,8 +64,8 @@ export class EuropeanController extends SessionController } public session: Session = { - accessToken: undefined, - refreshToken: undefined, + accessToken: '', + refreshToken: '', controlToken: undefined, deviceId: uuidV4(), tokenExpiresAt: 0, @@ -90,29 +87,35 @@ export class EuropeanController extends SessionController return 'Token not expired, no need to refresh'; } - const formData = new URLSearchParams(); - formData.append('grant_type', 'refresh_token'); - formData.append('redirect_uri', 'https://www.getpostman.com/oauth2/callback'); // Oversight from Hyundai developers - formData.append('refresh_token', this.session.refreshToken); - try { - const response = await got(this.environment.endpoints.token, { - 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', - }, + const uri = this.environment.loginFormHost + this.environment.endpoints.tokenURL; + + const headers = { + 'Content-type': 'application/x-www-form-urlencoded', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19_CCS_APP_AOS', + }; + + const formData = new URLSearchParams(); + formData.append('grant_type', 'refresh_token'); + formData.append('refresh_token', this.session.refreshToken); + formData.append('client_id', this.environment.ccspServiceID); + formData.append('client_secret', this.environment.ccspServiceSecret); + + const response = await got.post(uri, { + headers: headers, body: formData.toString(), + followRedirect: true, throwHttpErrors: false, }); if (response.statusCode !== 200) { - logger.debug(`Refresh token failed: ${response.body}`); - return `Refresh token failed: ${response.body}`; + logger.debug(`Refresh token failed: ${response.body}`); + return `Refresh token failed: ${response.body}`; + } + + const token = JSON.parse(response.body); + if (! token.refresh_token && this.session.refreshToken != '') { + token.refresh_token = this.session.refreshToken; } const responseBody = JSON.parse(response.body); @@ -160,7 +163,7 @@ export class EuropeanController extends SessionController if (!this.userConfig.password || !this.userConfig.username) { throw new Error('@EuropeController.login: username and password must be defined.'); } - let authResult: { code: Code; cookies: CookieJar } | null = null; + let authResult: { code: Code | Token; cookies: CookieJar } | null = null; try { logger.debug( `@EuropeController.login: Trying to sign in with ${this.authStrategies.main.name}` @@ -175,78 +178,47 @@ export class EuropeanController extends SessionController this.authStrategies.main.name } failed with error ${(e as Stringifiable).toString()}` ); - logger.debug( - `@EuropeController.login: Trying to sign in with ${this.authStrategies.fallback.name}` - ); - authResult = await this.authStrategies.fallback.login({ - password: this.userConfig.password, - username: this.userConfig.username, - }); + + throw new Error('@EuropeController.login: Could not manage to get token'); } + logger.debug('@EuropeController.login: Authenticated properly with user and password'); + + const token = authResult.code as Token; + this.session.accessToken = `Bearer ${token.access_token}`; + this.session.refreshToken = token.refresh_token; + this.session.tokenExpiresAt = Math.floor(Date.now() / 1000 + token.expires_in); + const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); - const notificationReponse = await got( - `${this.environment.baseUrl}/api/v1/spa/notifications/register`, - { - method: 'POST', - headers: { - 'ccsp-service-id': this.environment.clientId, - 'Content-Type': 'application/json;charset=UTF-8', - 'Host': this.environment.host, - 'Connection': 'Keep-Alive', - 'Accept-Encoding': 'gzip', - 'User-Agent': 'okhttp/3.10.0', - 'ccsp-application-id': this.environment.appId, - 'Stamp': await this.environment.stamp(), - }, - body: { - pushRegId: genRanHex(64), - pushType: 'APNS', - uuid: this.session.deviceId, - }, - json: true, - } - ); - if (notificationReponse) { - this.session.deviceId = notificationReponse.body.resMsg.deviceId; - } - logger.debug('@EuropeController.login: Device registered'); + const stamp: string = this.environment.stamp.result as string; - // 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 data = { + 'pushRegId': genRanHex(64), + 'pushType': this.environment.pushType, + 'uuid': crypto.randomUUID(), + }; - 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 headers = { + 'ccsp-service-id': this.environment.ccspServiceID, + 'ccsp-application-id': this.environment.ccspApplicationID, + 'Content-type': 'application/json;charset=UTF-8', + 'User-Agent': 'okhttp/3.10.0', + 'Stamp': stamp, + }; - const response = await got(tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'okhttp/3.10.0', - }, - body: tokenFormData.toString(), - cookieJar: authResult.cookies, - throwHttpErrors: false, - }); - if (response.statusCode !== 200) { - throw new Error(`@EuropeController.login: Could not manage to get token: ${response.body}`); - } + const notificationReponse = await got.post(this.environment.endpoints.deviceIdURL, { + headers: headers, + body: data, + json: true, + }); - if (response) { - const responseBody = JSON.parse(response.body); - this.session.accessToken = `Bearer ${responseBody.access_token}`; - this.session.refreshToken = responseBody.refresh_token; - this.session.tokenExpiresAt = Math.floor(Date.now() / 1000 + responseBody.expires_in); + if (notificationReponse) { + this.session.deviceId = notificationReponse.body.resMsg.deviceId; } + logger.debug('@EuropeController.login: Device registered'); logger.debug('@EuropeController.login: Session defined properly'); return 'Login success'; @@ -269,7 +241,7 @@ export class EuropeanController extends SessionController method: 'GET', headers: { ...this.defaultHeaders, - 'Stamp': await this.environment.stamp(), +// 'Stamp': await this.environment.stamp(), }, json: true, }); @@ -283,7 +255,7 @@ export class EuropeanController extends SessionController method: 'GET', headers: { ...this.defaultHeaders, - 'Stamp': await this.environment.stamp(), +// 'Stamp': await this.environment.stamp(), }, json: true, } @@ -329,7 +301,7 @@ export class EuropeanController extends SessionController headers: { ...this.defaultHeaders, 'Authorization': this.session.controlToken, - 'Stamp': await this.environment.stamp(), +// 'Stamp': await this.environment.stamp(), }, json: true, }); @@ -341,7 +313,7 @@ export class EuropeanController extends SessionController baseUrl: this.environment.baseUrl, headers: { ...this.defaultHeaders, - 'Stamp': await this.environment.stamp(), +// 'Stamp': await this.environment.stamp(), }, json: true, }); @@ -352,7 +324,7 @@ export class EuropeanController extends SessionController 'Authorization': this.session.accessToken, 'offset': (new Date().getTimezoneOffset() / 60).toFixed(2), 'ccsp-device-id': this.session.deviceId, - 'ccsp-application-id': this.environment.appId, + 'ccsp-application-id': this.environment.ccspApplicationID, 'Content-Type': 'application/json', }; }