bmw-connected-drive
Version:
This package can be used to access the BMW ConnectedDrive services.
222 lines • 10.7 kB
JavaScript
"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