@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
189 lines • 7.29 kB
JavaScript
"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