UNPKG

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