@shuangbing/bmw-connected-drive
Version:
This package can be used to access the BMW ConnectedDrive services.
187 lines • 8.98 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 LocalTokenStore_1 = require("./LocalTokenStore");
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) {
this.username = username;
this.password = password;
this.region = region;
this.tokenStore = tokenStore !== null && tokenStore !== void 0 ? tokenStore : new LocalTokenStore_1.LocalTokenStore();
this.logger = logger;
}
async getToken() {
var _a, _b, _c, _d, _e;
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 {
const refreshToken = this.token.refreshToken;
this.token = undefined;
this.token = await this.retrieveToken({
"grant_type": "refresh_token",
"refresh_token": refreshToken
});
}
catch {
// Intentional empty catch, as if the refresh failed, we can attempt a normal token retrieval.
}
}
else {
this.token = undefined;
}
}
if (!this.token || !((_d = this.token) === null || _d === void 0 ? void 0 : _d.accessToken)) {
(_e = this.logger) === null || _e === void 0 ? void 0 : _e.LogDebug("Getting token from token endpoint.");
this.token = await this.retrieveToken({
"grant_type": "authorization_code",
"username": this.username,
"password": this.password
});
}
if (!this.token) {
throw new Error("Error occurred while retrieving token.");
}
return this.token;
}
async retrieveToken(parameters) {
var _a, _b, _c, _d, _e, _f;
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.OAuthAuthorizationKey[this.region],
"x-user-agent": "android(v1.07_20200330);bmw;1.7.0(11152)"
},
credentials: "same-origin"
}, response => response.ok);
let data = await serverResponse.json();
const clientId = data.clientId;
const clientSecret = data.clientSecret;
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 authenticateUrl = `${data.gcdmBaseUrl}/gcdm/oauth/authenticate`;
const tokenUrl = data.tokenEndpoint;
const returnUrl = data.returnUrl;
const baseOAuthParams = {
client_id: clientId,
response_type: "code",
redirect_uri: returnUrl,
state: state,
nonce: "login_nonce",
scope: data.scopes.join(" "),
code_challenge: code_challenge,
code_challenge_method: "S256"
};
let body = { ...parameters, ...baseOAuthParams };
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogTrace(JSON.stringify(body));
serverResponse = await this.executeFetchWithRetry(authenticateUrl, {
method: "POST",
body: new url_1.URLSearchParams(body),
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
},
credentials: "same-origin"
}, response => response.ok);
data = await serverResponse.json();
const authorization = Account.getQueryStringValue(data.redirect_to, "authorization");
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.LogTrace(authorization);
body = { ...baseOAuthParams, ...{ authorization: authorization } };
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.LogTrace(JSON.stringify(body));
serverResponse = await this.executeFetchWithRetry(authenticateUrl, {
method: "POST",
body: new url_1.URLSearchParams(body),
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");
(_d = this.logger) === null || _d === void 0 ? void 0 : _d.LogTrace(nextUrl);
const code = Account.getQueryStringValue(nextUrl, "code");
(_e = this.logger) === null || _e === void 0 ? void 0 : _e.LogTrace(JSON.stringify(code));
const authHeaderValue = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
serverResponse = await this.executeFetchWithRetry(tokenUrl, {
method: "POST",
body: new url_1.URLSearchParams({
code: code,
code_verifier: code_verifier,
redirect_uri: 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();
this.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) {
(_f = this.logger) === null || _f === void 0 ? void 0 : _f.LogDebug("Storing token in token store.");
this.tokenStore.storeToken(this.token);
}
return this.token;
}
async executeFetchWithRetry(url, init, responseValidator) {
var _a;
let response;
let retryCount = 0;
do {
response = await fetch(url, init);
retryCount++;
} while (retryCount < 10 && !responseValidator(response) && (await this.delay(1000)));
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;
}
async delay(ms) {
var _a;
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.LogTrace("Sleeping for retry.");
await new Promise(resolve => setTimeout(resolve, ms));
return true;
}
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, '');
}
}
exports.Account = Account;
//# sourceMappingURL=Account.js.map