UNPKG

6.96 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2020, salesforce.com, inc.
4 * All rights reserved.
5 * Licensed under the BSD 3-Clause license.
6 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8/* eslint-disable camelcase */
9/* eslint-disable @typescript-eslint/ban-types */
10Object.defineProperty(exports, "__esModule", { value: true });
11exports.DeviceOauthService = void 0;
12// @ts-ignore
13const Transport = require("jsforce/lib/transport");
14const kit_1 = require("@salesforce/kit");
15const ts_types_1 = require("@salesforce/ts-types");
16const logger_1 = require("./logger");
17const authInfo_1 = require("./authInfo");
18const sfdxError_1 = require("./sfdxError");
19const connection_1 = require("./connection");
20async function wait(ms = 1000) {
21 return new Promise((resolve) => {
22 setTimeout(resolve, ms);
23 });
24}
25async function makeRequest(options) {
26 const rawResponse = await new Transport().httpRequest(options);
27 const response = kit_1.parseJsonMap(rawResponse.body);
28 if (response.error) {
29 const err = new sfdxError_1.SfdxError('Request Failed.');
30 err.data = Object.assign(response, { status: rawResponse.statusCode });
31 throw err;
32 }
33 else {
34 return response;
35 }
36}
37/**
38 * Handles device based login flows
39 *
40 * Usage:
41 * ```
42 * const oauthConfig = {
43 * loginUrl: this.flags.instanceurl,
44 * clientId: this.flags.clientid,
45 * };
46 * const deviceOauthService = await DeviceOauthService.create(oauthConfig);
47 * const loginData = await deviceOauthService.requestDeviceLogin();
48 * console.log(loginData);
49 * const approval = await deviceOauthService.awaitDeviceApproval(loginData);
50 * const authInfo = await deviceOauthService.authorizeAndSave(approval);
51 * ```
52 */
53class DeviceOauthService extends kit_1.AsyncCreatable {
54 constructor(options) {
55 super(options);
56 this.pollingCount = 0;
57 this.options = options;
58 if (!this.options.clientId)
59 this.options.clientId = authInfo_1.DEFAULT_CONNECTED_APP_INFO.clientId;
60 if (!this.options.loginUrl)
61 this.options.loginUrl = authInfo_1.AuthInfo.getDefaultInstanceUrl();
62 }
63 /**
64 * Begin the authorization flow by requesting the login
65 *
66 * @returns {Promise<DeviceCodeResponse>}
67 */
68 async requestDeviceLogin() {
69 const deviceFlowRequestUrl = this.getDeviceFlowRequestUrl();
70 const loginOptions = this.getLoginOptions(deviceFlowRequestUrl);
71 return makeRequest(loginOptions);
72 }
73 /**
74 * Polls the server until successful response OR max attempts have been made
75 *
76 * @returns {Promise<Nullable<DeviceCodePollingResponse>>}
77 */
78 async awaitDeviceApproval(loginData) {
79 const deviceFlowRequestUrl = this.getDeviceFlowRequestUrl();
80 const pollingOptions = this.getPollingOptions(deviceFlowRequestUrl, loginData.device_code);
81 const interval = kit_1.Duration.seconds(loginData.interval).milliseconds;
82 const response = await this.pollForDeviceApproval(pollingOptions, interval);
83 return response;
84 }
85 /**
86 * Creates and saves new AuthInfo
87 *
88 * @returns {Promise<AuthInfo>}
89 */
90 async authorizeAndSave(approval) {
91 const authInfo = await authInfo_1.AuthInfo.create({
92 oauth2Options: {
93 loginUrl: approval.instance_url,
94 refreshToken: approval.refresh_token,
95 clientSecret: this.options.clientSecret,
96 clientId: this.options.clientId,
97 },
98 });
99 await authInfo.save();
100 return authInfo;
101 }
102 async init() {
103 this.logger = await logger_1.Logger.child(this.constructor.name);
104 this.logger.debug(`this.options.clientId: ${this.options.clientId}`);
105 this.logger.debug(`this.options.loginUrl: ${this.options.loginUrl}`);
106 }
107 getLoginOptions(url) {
108 return {
109 url,
110 headers: connection_1.SFDX_HTTP_HEADERS,
111 method: 'POST',
112 form: {
113 client_id: ts_types_1.ensureString(this.options.clientId),
114 response_type: DeviceOauthService.RESPONSE_TYPE,
115 scope: DeviceOauthService.SCOPE,
116 },
117 };
118 }
119 getPollingOptions(url, code) {
120 return {
121 url,
122 headers: connection_1.SFDX_HTTP_HEADERS,
123 method: 'POST',
124 form: {
125 code,
126 grant_type: DeviceOauthService.GRANT_TYPE,
127 client_id: ts_types_1.ensureString(this.options.clientId),
128 },
129 };
130 }
131 getDeviceFlowRequestUrl() {
132 return `${ts_types_1.ensureString(this.options.loginUrl)}/services/oauth2/token`;
133 }
134 async poll(pollingOptions) {
135 this.logger.debug(`polling for device approval (attempt ${this.pollingCount} of ${DeviceOauthService.POLLING_COUNT_MAX})`);
136 try {
137 return await makeRequest(pollingOptions);
138 }
139 catch (e) {
140 const err = e.data;
141 if (err.error && err.status === 400 && err.error === 'authorization_pending') {
142 // do nothing because we're still waiting
143 }
144 else {
145 if (err.error && err.error_description) {
146 this.logger.error(`Polling error: ${err.error}: ${err.error_description}`);
147 }
148 else {
149 this.logger.error('Unknown Polling Error:');
150 this.logger.error(err);
151 }
152 throw err;
153 }
154 }
155 }
156 shouldContinuePolling() {
157 return this.pollingCount < DeviceOauthService.POLLING_COUNT_MAX;
158 }
159 async pollForDeviceApproval(pollingOptions, interval) {
160 this.logger.debug('BEGIN POLLING FOR DEVICE APPROVAL');
161 let result;
162 while (this.shouldContinuePolling()) {
163 result = await this.poll(pollingOptions);
164 if (result) {
165 this.logger.debug('POLLING FOR DEVICE APPROVAL SUCCESS');
166 break;
167 }
168 else {
169 this.logger.debug(`waiting ${interval} ms...`);
170 await wait(interval);
171 this.pollingCount += 1;
172 }
173 }
174 if (this.pollingCount >= DeviceOauthService.POLLING_COUNT_MAX) {
175 // stop polling, the user has likely abandoned the command...
176 this.logger.error(`Polling timed out because max polling was hit: ${this.pollingCount}`);
177 throw sfdxError_1.SfdxError.create('@salesforce/core', 'auth', 'pollingTimeout');
178 }
179 return result;
180 }
181}
182exports.DeviceOauthService = DeviceOauthService;
183DeviceOauthService.RESPONSE_TYPE = 'device_code';
184DeviceOauthService.GRANT_TYPE = 'device';
185DeviceOauthService.SCOPE = 'refresh_token web api';
186DeviceOauthService.POLLING_COUNT_MAX = 100;
187//# sourceMappingURL=deviceOauthService.js.map
\No newline at end of file