UNPKG

homebridge-adt-pulse

Version:
922 lines (921 loc) 59.9 kB
import axios from 'axios'; import { wrapper } from 'axios-cookiejar-support'; import { JSDOM } from 'jsdom'; import _ from 'lodash'; import { isErrorLike, serializeError } from 'serialize-error'; import { CookieJar } from 'tough-cookie'; import { detectGlobalDebugParser, detectGlobalPortalVersion } from './detect.js'; import { generateFakeDynatracePCHeaderValue, generateFakeLoginFingerprint } from './fake.js'; import { objectKeyClientType, objectKeyLocale, objectKeyLogin, objectKeyPreAuthToken, objectKeySat, requestPathAccessSignIn, requestPathAccessSignInEXxPartnerAdt, requestPathMfaMfaSignInWorkflowChallenge, requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthAddTrustedDeviceSatXx, requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthRequestOtpForRegisteredPropertySatXx, requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthValidateOtpSatXx, requestPathNgaServRunRraProxyHrefRestIcontrolUiClientMultiFactorAuthSatXx, requestPathNgaServRunRraProxyOnlyClientMultiFactorAuthExcludeSatXxHrefRestAdtUiUpdatesSatXx, requestPathSummarySummary, requestPathSystemSystem, textOneTimePasscode, } from './regex.js'; import { multiFactorAuth, otpResponse } from './schema.js'; import { debugLog, fetchErrorMessage, findNullKeys, generateHash, parseMultiFactorMethods, parseMultiFactorTrustedDevices, parseSensorsTable, stackTracer, } from './utility.js'; export class ADTPulseAuth { #connection; #credentials; #internal; #session; constructor(config, internalConfig) { this.#connection = { subdomain: config.subdomain, }; this.#credentials = { fingerprint: generateFakeLoginFingerprint(), password: config.password, username: config.username, }; this.#internal = { baseUrl: internalConfig.baseUrl ?? `https://${this.#connection.subdomain}.adtpulse.com`, debug: internalConfig.debug ?? false, logger: internalConfig.logger ?? null, reportedHashes: [], }; this.#session = { httpClient: wrapper(axios.create({ jar: new CookieJar(), validateStatus: () => true, })), mfa: { clientType: null, locale: null, login: null, preAuthToken: null, satCode: null, token: null, trustedDevices: [], verificationMethods: [], }, portalVersion: null, status: 'logged-out', }; } async getVerificationMethods() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Attempting to retrieve verification methods from "${this.#internal.baseUrl}"`); } try { const sessions = {}; if (this.#session.status === 'complete' || this.#session.status === 'not-required') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', 'Already retrieved verification methods'); } return { action: 'GET_VERIFICATION_METHODS', success: true, info: { methods: this.#session.mfa.verificationMethods, status: this.#session.status, }, }; } sessions.axiosIndex = await this.#session.httpClient.get(`${this.#internal.baseUrl}/`, this.getRequestConfig()); if (sessions.axiosIndex.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`, }, }; } if (typeof sessions.axiosIndex?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosIndexRequestPath = sessions.axiosIndex.request.path; const axiosIndexRequestPathValid = requestPathAccessSignIn.test(axiosIndexRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path ➜ ${axiosIndexRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path valid ➜ ${axiosIndexRequestPathValid}`); } if (!axiosIndexRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `"${axiosIndexRequestPath} is not the sign-in page`); } this.handleLoginFailure(axiosIndexRequestPath, sessions.axiosIndex); return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: `"${axiosIndexRequestPath} is not the sign-in page`, }, }; } const loginForm = new URLSearchParams(); loginForm.append('usernameForm', this.#credentials.username); loginForm.append('passwordForm', this.#credentials.password); loginForm.append('sun', 'yes'); loginForm.append('networkid', ''); loginForm.append('fingerprint', this.#credentials.fingerprint); this.#session.portalVersion = axiosIndexRequestPath.replace(requestPathAccessSignIn, '$2'); await this.newInformationDispatcher('portal-version', { version: this.#session.portalVersion }); sessions.axiosSignIn = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signin.jsp?e=ns&partner=adt`, loginForm, this.getRequestConfig({ headers: { 'Cache-Control': 'max-age=0', 'Content-Type': 'application/x-www-form-urlencoded', Origin: this.#internal.baseUrl, Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signin.jsp?e=ns&partner=adt`, 'Sec-Fetch-Site': 'same-origin', }, })); if (sessions.axiosSignIn.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `The remote server responded with a HTTP ${sessions.axiosSignIn.status} status code`); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosSignIn.status} status code`, }, }; } if (typeof sessions.axiosSignIn?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosSignInRequestPath = sessions.axiosSignIn.request.path; const axiosSignInRequestPathValid = requestPathMfaMfaSignInWorkflowChallenge.test(axiosSignInRequestPath) || requestPathSummarySummary.test(axiosSignInRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path ➜ ${axiosSignInRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path valid ➜ ${axiosSignInRequestPathValid}`); } if (requestPathSummarySummary.test(axiosSignInRequestPath)) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Verification methods not required from "${this.#internal.baseUrl}"`); } this.#session.status = 'not-required'; return { action: 'GET_VERIFICATION_METHODS', success: true, info: { methods: this.#session.mfa.verificationMethods, status: this.#session.status, }, }; } if (requestPathAccessSignIn.test(axiosSignInRequestPath) || requestPathAccessSignInEXxPartnerAdt.test(axiosSignInRequestPath)) { const errorMessage = fetchErrorMessage(sessions.axiosSignIn); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', errorMessage ?? 'Unknown error'); } this.#session.status = 'logged-out'; return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: errorMessage ?? 'Unknown error', }, }; } if (!axiosSignInRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `"${axiosSignInRequestPath}" is not the workflow challenge page`); } this.handleLoginFailure(axiosSignInRequestPath, sessions.axiosSignIn); return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: `"${axiosSignInRequestPath}" is not the workflow challenge page`, }, }; } if (typeof sessions.axiosSignIn.data !== 'string') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The response body of the workflow challenge page is not of type "string"'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'The response body of the workflow challenge page is not of type "string"', }, }; } const matchClientType = sessions.axiosSignIn.data.match(objectKeyClientType); const matchLocale = sessions.axiosSignIn.data.match(objectKeyLocale); const matchLogin = sessions.axiosSignIn.data.match(objectKeyLogin); const matchPreAuthToken = sessions.axiosSignIn.data.match(objectKeyPreAuthToken); const matchSatCode = sessions.axiosSignIn.data.match(objectKeySat); this.#session.mfa.clientType = (matchClientType !== null && matchClientType.length >= 2) ? matchClientType[1] : null; this.#session.mfa.locale = (matchLocale !== null && matchLocale.length >= 2) ? matchLocale[1] : null; this.#session.mfa.login = (matchLogin !== null && matchLogin.length >= 2) ? matchLogin[1] : null; this.#session.mfa.preAuthToken = (matchPreAuthToken !== null && matchPreAuthToken.length >= 2) ? matchPreAuthToken[1] : null; this.#session.mfa.satCode = (matchSatCode !== null && matchSatCode.length >= 2) ? matchSatCode[1] : null; if (this.#session.mfa.clientType === null || this.#session.mfa.locale === null || this.#session.mfa.login === null || this.#session.mfa.preAuthToken === null || this.#session.mfa.satCode === null) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'Failed to retrieve required MFA details from the workflow challenge page'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'Failed to retrieve required MFA details from the workflow challenge page', }, }; } sessions.axiosMethods = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/icontrol/ui/client/multiFactorAuth&sat=${this.#session.mfa.satCode}`, this.getRequestConfig({ headers: { Accept: 'application/json', 'Content-Type': undefined, Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': undefined, 'Upgrade-Insecure-Requests': undefined, 'X-clientType': this.#session.mfa.clientType, 'X-format': 'json', 'X-locale': this.#session.mfa.locale, 'X-login': this.#session.mfa.login, 'X-preAuthToken': this.#session.mfa.preAuthToken, 'X-version': '7.0', 'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'), }, })); if (sessions.axiosMethods.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `The remote server responded with a HTTP ${sessions.axiosMethods.status} status code`); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosMethods.status} status code`, }, }; } if (typeof sessions.axiosMethods?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosMethodsRequestPath = sessions.axiosMethods.request.path; const axiosMethodsRequestPathValid = requestPathNgaServRunRraProxyHrefRestIcontrolUiClientMultiFactorAuthSatXx.test(axiosMethodsRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path ➜ ${axiosMethodsRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path valid ➜ ${axiosMethodsRequestPathValid}`); } if (!axiosMethodsRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `"${axiosMethodsRequestPath}" is not the MFA auth page`); } this.handleLoginFailure(axiosMethodsRequestPath, sessions.axiosMethods); return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: `"${axiosMethodsRequestPath}" is not the MFA auth page`, }, }; } const parsedMethods = multiFactorAuth.safeParse(sessions.axiosMethods.data); if (!parsedMethods.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The response body of the MFA auth page is invalid'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'The response body of the MFA auth page is invalid', }, }; } const parsedMultiFactorMethods = parseMultiFactorMethods(parsedMethods.data); if (parsedMultiFactorMethods.length === 0) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'Failed to retrieve verification methods'); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { message: 'Failed to retrieve verification methods', }, }; } this.#session.mfa.verificationMethods = parsedMultiFactorMethods; this.#session.status = 'complete'; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'success', `Successfully retrieved verification methods from "${this.#internal.baseUrl}"`); } return { action: 'GET_VERIFICATION_METHODS', success: true, info: { methods: parsedMultiFactorMethods, status: this.#session.status, }, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'GET_VERIFICATION_METHODS', success: false, info: { error: errorObject, }, }; } async requestCode(methodId) { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'info', `Attempting to request a code using the "${methodId}" method at "${this.#internal.baseUrl}"`); } if (this.#session.mfa.verificationMethods.find((device) => device.id === methodId) === undefined) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', `The "${methodId}" verification method does not exist. Did you run the "getVerificationMethods()" yet?`); } return { action: 'REQUEST_CODE', success: false, info: { message: `The "${methodId}" verification method does not exist. Did you run the "getVerificationMethods()" yet?`, }, }; } try { const sessions = {}; const requestCodeForm = new URLSearchParams(); requestCodeForm.append('id', methodId); sessions.axiosRequestCode = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/adt/ui/client/multiFactorAuth/requestOtpForRegisteredProperty&sat=${this.#session.mfa.satCode}`, requestCodeForm, this.getRequestConfig({ headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Origin: this.#internal.baseUrl, Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': undefined, 'Upgrade-Insecure-Requests': undefined, 'X-clientType': this.#session.mfa.clientType, 'X-format': 'json', 'X-locale': this.#session.mfa.locale, 'X-login': this.#session.mfa.login, 'X-preAuthToken': this.#session.mfa.preAuthToken, 'X-version': '7.0', 'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'), }, })); if (typeof sessions.axiosRequestCode?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'REQUEST_CODE', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosRequestCodeRequestPath = sessions.axiosRequestCode.request.path; const axiosRequestCodeRequestPathValid = requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthRequestOtpForRegisteredPropertySatXx.test(axiosRequestCodeRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'info', `Request path ➜ ${axiosRequestCodeRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'info', `Request path valid ➜ ${axiosRequestCodeRequestPathValid}`); } if (!axiosRequestCodeRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', `"${axiosRequestCodeRequestPath}" is not the request code page`); } this.handleLoginFailure(axiosRequestCodeRequestPath, sessions.axiosRequestCode); return { action: 'REQUEST_CODE', success: false, info: { message: `"${axiosRequestCodeRequestPath}" is not the request code page`, }, }; } const parsedRequestCode = otpResponse.safeParse(sessions.axiosRequestCode.data); if (!parsedRequestCode.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', 'The response body of the request code page is invalid'); } return { action: 'REQUEST_CODE', success: false, info: { message: 'The response body of the request code page is invalid', }, }; } if (!parsedRequestCode.data.detail.includes('OK')) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', `Unable to get a verification code ➜ "${parsedRequestCode.data.detail}"`); } return { action: 'REQUEST_CODE', success: false, info: { message: `Unable to get a verification code ➜ "${parsedRequestCode.data.detail}"`, }, }; } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'success', `Successfully requested a code for "${methodId}" at "${this.#internal.baseUrl}"`); } return { action: 'REQUEST_CODE', success: true, info: null, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'REQUEST_CODE', success: false, info: { error: errorObject, }, }; } async validateCode(otpCode) { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'info', `Attempting to validate verification code at "${this.#internal.baseUrl}"`); } if (!textOneTimePasscode.test(otpCode)) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', `"${otpCode}" is an invalid verification code format`); } return { action: 'VALIDATE_CODE', success: false, info: { message: `"${otpCode}" is an invalid verification code format`, }, }; } try { const sessions = {}; const validateCodeForm = new URLSearchParams(); validateCodeForm.append('otp', otpCode); sessions.axiosValidateCode = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/adt/ui/client/multiFactorAuth/validateOtp&sat=${this.#session.mfa.satCode}`, validateCodeForm, this.getRequestConfig({ headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Origin: this.#internal.baseUrl, Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': undefined, 'Upgrade-Insecure-Requests': undefined, 'X-clientType': this.#session.mfa.clientType, 'X-format': 'json', 'X-locale': this.#session.mfa.locale, 'X-login': this.#session.mfa.login, 'X-preAuthToken': this.#session.mfa.preAuthToken, 'X-version': '7.0', 'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'), }, })); if (typeof sessions.axiosValidateCode?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'VALIDATE_CODE', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosValidateCodeRequestPath = sessions.axiosValidateCode.request.path; const axiosValidateCodeRequestPathValid = requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthValidateOtpSatXx.test(axiosValidateCodeRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'info', `Request path ➜ ${axiosValidateCodeRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'info', `Request path valid ➜ ${axiosValidateCodeRequestPathValid}`); } if (!axiosValidateCodeRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', `"${axiosValidateCodeRequestPath}" is not the validate code page`); } this.handleLoginFailure(axiosValidateCodeRequestPath, sessions.axiosValidateCode); return { action: 'VALIDATE_CODE', success: false, info: { message: `"${axiosValidateCodeRequestPath}" is not the validate code page`, }, }; } if (typeof sessions.axiosValidateCode.data === 'string' && sessions.axiosValidateCode.data === '') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'Already passed verification'); } return { action: 'VALIDATE_CODE', success: true, info: null, }; } const parsedValidateCode = otpResponse.safeParse(sessions.axiosValidateCode.data); if (!parsedValidateCode.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'The response body of the validate code page is invalid'); } return { action: 'VALIDATE_CODE', success: false, info: { message: 'The response body of the validate code page is invalid', }, }; } if (!parsedValidateCode.data.detail.startsWith('u=')) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'The verification code submitted is either invalidated or expired'); } return { action: 'VALIDATE_CODE', success: false, info: { message: 'The verification code submitted is either invalidated or expired', }, }; } this.#session.mfa.token = parsedValidateCode.data.detail; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'success', `Successfully validated verification code at "${this.#internal.baseUrl}"`); } return { action: 'VALIDATE_CODE', success: true, info: null, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'VALIDATE_CODE', success: false, info: { error: errorObject, }, }; } async getTrustedDevices() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'info', `Attempting to retrieve trusted devices at "${this.#internal.baseUrl}"`); } try { const sessions = {}; sessions.axiosDevicePoll = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?only=client.multiFactorAuth&exclude=&sat=${this.#session.mfa.satCode}&href=rest/adt/ui/updates&sat=${this.#session.mfa.satCode}&`, this.getRequestConfig({ headers: { Accept: 'application/json', Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': undefined, 'Upgrade-Insecure-Requests': undefined, 'X-clientType': this.#session.mfa.clientType, 'X-format': 'json', 'X-locale': this.#session.mfa.locale, 'X-login': this.#session.mfa.login, 'X-token': this.#session.mfa.token, 'X-version': '7.0', 'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'), }, })); if (typeof sessions.axiosDevicePoll?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_TRUSTED_DEVICES', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosDevicePollRequestPath = sessions.axiosDevicePoll.request.path; const axiosDevicePollRequestPathValid = requestPathNgaServRunRraProxyOnlyClientMultiFactorAuthExcludeSatXxHrefRestAdtUiUpdatesSatXx.test(axiosDevicePollRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'info', `Request path ➜ ${axiosDevicePollRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'info', `Request path valid ➜ ${axiosDevicePollRequestPathValid}`); } if (!axiosDevicePollRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', `"${axiosDevicePollRequestPath}" is not the device polling page`); } this.handleLoginFailure(axiosDevicePollRequestPath, sessions.axiosDevicePoll); return { action: 'GET_TRUSTED_DEVICES', success: false, info: { message: `"${axiosDevicePollRequestPath}" is not the device polling page`, }, }; } const parsedDevicePoll = multiFactorAuth.safeParse(_.get(sessions.axiosDevicePoll.data, ['update', 0, 'data', 'client', 'multiFactorAuth'])); if (!parsedDevicePoll.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', 'The response body of the device polling page is invalid'); } return { action: 'GET_TRUSTED_DEVICES', success: false, info: { message: 'The response body of the device polling page is invalid', }, }; } const parsedTrustedDevices = parseMultiFactorTrustedDevices(parsedDevicePoll.data); this.#session.mfa.trustedDevices = parsedTrustedDevices; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'success', `Successfully retrieved trusted devices at "${this.#internal.baseUrl}"`); } return { action: 'GET_TRUSTED_DEVICES', success: true, info: { trustedDevices: parsedTrustedDevices, }, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'GET_TRUSTED_DEVICES', success: false, info: { error: errorObject, }, }; } async addTrustedDevice(deviceName) { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'info', `Attempting to add trusted device at "${this.#internal.baseUrl}"`); } if (this.#session.mfa.trustedDevices.find((trustedDevice) => trustedDevice.name === encodeURIComponent(deviceName)) !== undefined) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The name for the trusted device already exists'); } return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { message: 'The name for the trusted device already exists', }, }; } if (deviceName.length < 1 || deviceName.length > 100) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The name for the trusted device must be between 1 to 100 characters'); } return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { message: 'The name for the trusted device must be between 1 to 100 characters', }, }; } try { const sessions = {}; const trustedDeviceForm = new URLSearchParams(); trustedDeviceForm.append('name', encodeURIComponent(deviceName)); sessions.axiosAddDevice = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/adt/ui/client/multiFactorAuth/addTrustedDevice&sat=${this.#session.mfa.satCode}`, trustedDeviceForm, this.getRequestConfig({ headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Origin: this.#internal.baseUrl, Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`, 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': undefined, 'Upgrade-Insecure-Requests': undefined, 'X-clientType': this.#session.mfa.clientType, 'X-format': 'json', 'X-locale': this.#session.mfa.locale, 'X-login': this.#session.mfa.login, 'X-token': this.#session.mfa.token, 'X-version': '7.0', 'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'), }, })); if (typeof sessions.axiosAddDevice?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosAddDeviceRequestPath = sessions.axiosAddDevice.request.path; const axiosAddDeviceRequestPathValid = requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthAddTrustedDeviceSatXx.test(axiosAddDeviceRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'info', `Request path ➜ ${axiosAddDeviceRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'info', `Request path valid ➜ ${axiosAddDeviceRequestPathValid}`); } if (!axiosAddDeviceRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', `"${axiosAddDeviceRequestPath}" is not the add trusted device page`); } this.handleLoginFailure(axiosAddDeviceRequestPath, sessions.axiosAddDevice); return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { message: `"${axiosAddDeviceRequestPath}" is not the add trusted device page`, }, }; } const parsedValidateCode = otpResponse.safeParse(sessions.axiosAddDevice.data); if (!parsedValidateCode.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The response body of the add trusted device page is invalid'); } return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { message: 'The response body of the add trusted device page is invalid', }, }; } if (!parsedValidateCode.data.detail.includes('OK')) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', `The trusted device could not be added ➜ "${parsedValidateCode.data.detail}"`); } return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { message: `The trusted device could not be added ➜ "${parsedValidateCode.data.detail}"`, }, }; } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'success', `Successfully added trusted device at "${this.#internal.baseUrl}"`); } return { action: 'ADD_TRUSTED_DEVICE', success: true, info: null, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'ADD_TRUSTED_DEVICE', success: false, info: { error: errorObject, }, }; } async completeSignIn() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'info', `Attempting to complete sign in at "${this.#internal.baseUrl}"`); } try { const sessions = {}; sessions.axiosPostSignIn = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/PostSigninProcessServ`, this.getRequestConfig({ headers: { Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`, 'Sec-Fetch-Site': 'same-origin', }, })); if (sessions.axiosPostSignIn.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', `The remote server responded with a HTTP ${sessions.axiosPostSignIn.status} status code`); } return { action: 'COMPLETE_SIGN_IN', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosPostSignIn.status} status code`, }, }; } if (typeof sessions.axiosPostSignIn?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'COMPLETE_SIGN_IN', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosPostSignInRequestPath = sessions.axiosPostSignIn.request.path; const axiosPostSignInRequestPathValid = requestPathSummarySummary.test(axiosPostSignInRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'info', `Request path ➜ ${axiosPostSignInRequestPath}`); debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'info', `Request path valid ➜ ${axiosPostSignInRequestPathValid}`); } if (!axiosPostSignInRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', `"${axiosPostSignInRequestPath}" is not the summary page`); } this.handleLoginFailure(axiosPostSignInRequestPath, sessions.axiosPostSignIn); return { action: 'COMPLETE_SIGN_IN', success: false, info: { message: `"${axiosPostSignInRequestPath}" is not the summary page`, }, }; } this.#session.mfa = { clientType: null, locale: null, login: null, preAuthToken: null, satCode: null, token: null, trustedDevices: [], verificationMethods: [], }; if (this.#internal.debug) { debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'success', `Successfully completed sign in at "${this.#internal.baseUrl}"`); } return { action: 'COMPLETE_SIGN_IN', success: true, info: null, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) {