UNPKG

bmw-connected-drive

Version:

This package can be used to access the BMW ConnectedDrive services.

222 lines 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Account = void 0; const Constants_1 = require("./Constants"); const Token_1 = require("./Token"); const LocalTokenStore_1 = require("./LocalTokenStore"); const Utils_1 = require("./Utils"); const uuid_1 = require("uuid"); const crypto_1 = __importDefault(require("crypto")); const url_1 = require("url"); const crossFetch = require('cross-fetch'); const fetch = require('fetch-cookie')(crossFetch); class Account { constructor(username, password, region, tokenStore, logger, captchaToken) { this.username = username; this.password = password; this.region = region; this.tokenStore = tokenStore !== null && tokenStore !== void 0 ? tokenStore : new LocalTokenStore_1.LocalTokenStore(); this.logger = logger; this.captchaToken = captchaToken; } async getToken() { var _a, _b, _c, _d, _f, _g, _h; if (!this.token && this.tokenStore) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogDebug("Attempting retrieving token from token store."); this.token = this.tokenStore.retrieveToken(); } if (this.token && new Date() > this.token.validUntil) { (_b = this.logger) === null || _b === void 0 ? void 0 : _b.LogDebug("Token expired."); if (this.token.refreshToken) { (_c = this.logger) === null || _c === void 0 ? void 0 : _c.LogDebug("Attempting refreshing."); try { this.token = await this.refresh_token(this.token); } catch (_e) { this.token = undefined; (_d = this.logger) === null || _d === void 0 ? void 0 : _d.LogError("Error occurred while refreshing token. Attempting normal token retrieval."); let e = _e; if (e) { (_f = this.logger) === null || _f === void 0 ? void 0 : _f.LogError(e.message); } } } else { this.token = undefined; } } if (!this.token || !this.token.accessToken) { if (this.captchaToken) { (_g = this.logger) === null || _g === void 0 ? void 0 : _g.LogDebug("Getting token from token endpoint."); this.token = await this.login(this.username, this.password, this.captchaToken); this.captchaToken = undefined; // Delete because the captcha token is only valid for a short time and can only be used once } else { (_h = this.logger) === null || _h === void 0 ? void 0 : _h.LogDebug("Missing captcha token for first authentication."); } } if (!this.token) { throw new Error("Error occurred while retrieving token."); } return this.token; } async login(username, password, captchaToken) { var _a, _b, _c; const oauthConfig = await this.retrieveOAuthConfig(); const code_verifier = Account.base64UrlEncode(crypto_1.default.randomBytes(64)); const hash = crypto_1.default.createHash('sha256'); const code_challenge = Account.base64UrlEncode(hash.update(code_verifier).digest()); const state = Account.base64UrlEncode(crypto_1.default.randomBytes(16)); const baseOAuthParams = { client_id: oauthConfig.clientId, response_type: "code", redirect_uri: oauthConfig.returnUrl, state: state, nonce: "login_nonce", scope: oauthConfig.scopes.join(" "), code_challenge: code_challenge, code_challenge_method: "S256" }; const authenticateUrl = oauthConfig.tokenEndpoint.replace("/token", "/authenticate"); const headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "hcaptchatoken": captchaToken }; let serverResponse = await this.executeFetchWithRetry(authenticateUrl, { method: "POST", body: new url_1.URLSearchParams({ ...{ grant_type: "authorization_code", username: username, password: password }, ...baseOAuthParams }), headers: headers, credentials: "same-origin" }, response => response.ok); let data = await serverResponse.json(); const authorization = Account.getQueryStringValue(data.redirect_to, "authorization"); (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogTrace(authorization); serverResponse = await this.executeFetchWithRetry(authenticateUrl, { method: "POST", body: new url_1.URLSearchParams({ ...baseOAuthParams, ...{ authorization: authorization } }), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, redirect: "manual", credentials: "same-origin" }, response => response.status === 302); const nextUrl = serverResponse.headers.get("location"); (_b = this.logger) === null || _b === void 0 ? void 0 : _b.LogTrace(nextUrl); const code = Account.getQueryStringValue(nextUrl, "code"); (_c = this.logger) === null || _c === void 0 ? void 0 : _c.LogTrace(JSON.stringify(code)); const authHeaderValue = Buffer.from(`${oauthConfig.clientId}:${oauthConfig.clientSecret}`).toString('base64'); serverResponse = await this.executeFetchWithRetry(oauthConfig.tokenEndpoint, { method: "POST", body: new url_1.URLSearchParams({ code: code, code_verifier: code_verifier, redirect_uri: oauthConfig.returnUrl, grant_type: "authorization_code" }), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", authorization: `Basic ${authHeaderValue}` }, credentials: "same-origin" }, response => response.ok); data = await serverResponse.json(); return this.buildTokenAndStore(data); } async refresh_token(token) { const oauthConfig = await this.retrieveOAuthConfig(); const authHeaderValue = Buffer.from(`${oauthConfig.clientId}:${oauthConfig.clientSecret}`).toString('base64'); let serverResponse = await this.executeFetchWithRetry(oauthConfig.tokenEndpoint, { method: "POST", body: new url_1.URLSearchParams({ grant_type: "refresh_token", refresh_token: token.refreshToken, scope: oauthConfig.scopes.join(" "), redirect_uri: oauthConfig.returnUrl, }), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", authorization: `Basic ${authHeaderValue}` }, credentials: "same-origin" }, response => response.ok); let data = await serverResponse.json(); return this.buildTokenAndStore(data); } async retrieveOAuthConfig() { const authSettingsUrl = `https://${Constants_1.Constants.ServerEndpoints[this.region]}/eadrax-ucs/v1/presentation/oauth/config`; let serverResponse = await this.executeFetchWithRetry(authSettingsUrl, { method: "GET", headers: { "ocp-apim-subscription-key": Constants_1.Constants.ApimSubscriptionKey[this.region], "x-identity-provider": "gcdm", }, credentials: "same-origin" }, response => response.ok); return await serverResponse.json(); } async executeFetchWithRetry(url, init, responseValidator) { var _a; const correlationId = (0, uuid_1.v4)(); let response; let retryCount = 0; init.headers["user-agent"] = Constants_1.Constants.User_Agent; init.headers["x-user-agent"] = Constants_1.Constants.X_User_Agent(this.region); init.headers["x-identity-provider"] = "gcdm"; init.headers["bmw-session-id"] = correlationId; init.headers["x-correlation-id"] = correlationId; init.headers["bmw-correlation-id"] = correlationId; do { response = await fetch(url, init); retryCount++; } while (retryCount < 10 && !responseValidator(response) && (await Utils_1.Utils.Delay(1000, this.logger))); if (!responseValidator(response)) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogError(`${response.status}: Error occurred while attempting to retrieve token. Server response: ${(await response.text())}`); throw new Error(`${response.status}: Error occurred while attempting to retrieve token.`); } return response; } static getQueryStringValue(url, queryParamName) { const splitUrl = url === null || url === void 0 ? void 0 : url.split("?"); const queryString = splitUrl.length > 1 ? splitUrl[1] : splitUrl[0]; const parsedQueryString = queryString === null || queryString === void 0 ? void 0 : queryString.split("&"); if (!parsedQueryString) { throw new Error(`Url: '${url}' does not contain query string.`); } for (const param of parsedQueryString) { const paramKeyValue = param.split("="); if (paramKeyValue[0].toLowerCase() === queryParamName) { return paramKeyValue[1]; } } throw new Error(`Url: '${url}' does not contain parameter '${queryParamName}'.`); } static base64UrlEncode(buffer) { return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } buildTokenAndStore(data) { var _a; let token = new Token_1.Token({ response: JSON.stringify(data), accessToken: data.access_token, refreshToken: data.refresh_token, validUntil: new Date(new Date().getTime() + ((data.expires_in - 5) * 1000)) }); if (this.tokenStore) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogDebug("Storing token in token store."); this.tokenStore.storeToken(token); } return token; } } exports.Account = Account; //# sourceMappingURL=Account.js.map