UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

189 lines 7.29 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ /* eslint-disable camelcase */ /* eslint-disable @typescript-eslint/ban-types */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceOauthService = void 0; const transport_1 = require("jsforce/lib/transport"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const FormData = require("form-data"); const logger_1 = require("./logger"); const org_1 = require("./org"); const sfError_1 = require("./sfError"); const messages_1 = require("./messages"); messages_1.Messages.importMessagesDirectory(__dirname); const messages = messages_1.Messages.load('@salesforce/core', 'auth', ['pollingTimeout']); async function wait(ms = 1000) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function makeRequest(options) { const rawResponse = await new transport_1.default().httpRequest(options); const response = (0, kit_1.parseJsonMap)(rawResponse.body); if (response.error) { const err = new sfError_1.SfError('Request Failed.'); err.data = Object.assign(response, { status: rawResponse.statusCode }); throw err; } else { return response; } } /** * Handles device based login flows * * Usage: * ``` * const oauthConfig = { * loginUrl: this.flags.instanceurl, * clientId: this.flags.clientid, * }; * const deviceOauthService = await DeviceOauthService.create(oauthConfig); * const loginData = await deviceOauthService.requestDeviceLogin(); * console.log(loginData); * const approval = await deviceOauthService.awaitDeviceApproval(loginData); * const authInfo = await deviceOauthService.authorizeAndSave(approval); * ``` */ class DeviceOauthService extends kit_1.AsyncCreatable { constructor(options) { super(options); this.pollingCount = 0; this.options = options; if (!this.options.clientId) this.options.clientId = org_1.DEFAULT_CONNECTED_APP_INFO.clientId; if (!this.options.loginUrl) this.options.loginUrl = org_1.AuthInfo.getDefaultInstanceUrl(); } /** * Begin the authorization flow by requesting the login * * @returns {Promise<DeviceCodeResponse>} */ async requestDeviceLogin() { const deviceFlowRequestUrl = this.getDeviceFlowRequestUrl(); const loginOptions = this.getLoginOptions(deviceFlowRequestUrl); return makeRequest(loginOptions); } /** * Polls the server until successful response OR max attempts have been made * * @returns {Promise<Nullable<DeviceCodePollingResponse>>} */ async awaitDeviceApproval(loginData) { const deviceFlowRequestUrl = this.getDeviceFlowRequestUrl(); const pollingOptions = this.getPollingOptions(deviceFlowRequestUrl, loginData.device_code); const interval = kit_1.Duration.seconds(loginData.interval).milliseconds; return await this.pollForDeviceApproval(pollingOptions, interval); } /** * Creates and saves new AuthInfo * * @returns {Promise<AuthInfo>} */ async authorizeAndSave(approval) { const authInfo = await org_1.AuthInfo.create({ oauth2Options: { loginUrl: approval.instance_url, refreshToken: approval.refresh_token, clientSecret: this.options.clientSecret, clientId: this.options.clientId, }, }); await authInfo.save(); return authInfo; } async init() { this.logger = await logger_1.Logger.child(this.constructor.name); this.logger.debug(`this.options.clientId: ${this.options.clientId}`); this.logger.debug(`this.options.loginUrl: ${this.options.loginUrl}`); } getLoginOptions(url) { const form = new FormData(); form.append('client_id', (0, ts_types_1.ensureString)(this.options.clientId)); form.append('response_type', DeviceOauthService.RESPONSE_TYPE); form.append('scope', DeviceOauthService.SCOPE); return { url, headers: { ...org_1.SFDX_HTTP_HEADERS, ...form.getHeaders() }, method: 'POST', body: form.getBuffer(), }; } getPollingOptions(url, code) { const form = new FormData(); form.append('client_id', (0, ts_types_1.ensureString)(this.options.clientId)); form.append('grant_type', DeviceOauthService.GRANT_TYPE); form.append('code', code); return { url, headers: { ...org_1.SFDX_HTTP_HEADERS, ...form.getHeaders() }, method: 'POST', body: form.getBuffer(), }; } getDeviceFlowRequestUrl() { return `${(0, ts_types_1.ensureString)(this.options.loginUrl)}/services/oauth2/token`; } async poll(httpRequest) { this.logger.debug(`polling for device approval (attempt ${this.pollingCount} of ${DeviceOauthService.POLLING_COUNT_MAX})`); try { return await makeRequest(httpRequest); } catch (e) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const err = e.data; if (err.error && err.status === 400 && err.error === 'authorization_pending') { // do nothing because we're still waiting } else { if (err.error && err.error_description) { this.logger.error(`Polling error: ${err.error}: ${err.error_description}`); } else { this.logger.error('Unknown Polling Error:'); this.logger.error(err); } throw err; } } } shouldContinuePolling() { return this.pollingCount < DeviceOauthService.POLLING_COUNT_MAX; } async pollForDeviceApproval(httpRequest, interval) { this.logger.debug('BEGIN POLLING FOR DEVICE APPROVAL'); let result; while (this.shouldContinuePolling()) { result = await this.poll(httpRequest); if (result) { this.logger.debug('POLLING FOR DEVICE APPROVAL SUCCESS'); break; } else { this.logger.debug(`waiting ${interval} ms...`); await wait(interval); this.pollingCount += 1; } } if (this.pollingCount >= DeviceOauthService.POLLING_COUNT_MAX) { // stop polling, the user has likely abandoned the command... this.logger.error(`Polling timed out because max polling was hit: ${this.pollingCount}`); throw messages.createError('pollingTimeout'); } return result; } } exports.DeviceOauthService = DeviceOauthService; DeviceOauthService.RESPONSE_TYPE = 'device_code'; DeviceOauthService.GRANT_TYPE = 'device'; DeviceOauthService.SCOPE = 'refresh_token web api'; DeviceOauthService.POLLING_COUNT_MAX = 100; //# sourceMappingURL=deviceOauthService.js.map