1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | Object.defineProperty(exports, "__esModule", { value: true });
|
11 | exports.DeviceOauthService = void 0;
|
12 | const transport_1 = require("jsforce/lib/transport");
|
13 | const kit_1 = require("@salesforce/kit");
|
14 | const ts_types_1 = require("@salesforce/ts-types");
|
15 | const FormData = require("form-data");
|
16 | const logger_1 = require("./logger");
|
17 | const org_1 = require("./org");
|
18 | const sfError_1 = require("./sfError");
|
19 | const messages_1 = require("./messages");
|
20 | messages_1.Messages.importMessagesDirectory(__dirname);
|
21 | const messages = messages_1.Messages.load('@salesforce/core', 'auth', ['pollingTimeout']);
|
22 | async function wait(ms = 1000) {
|
23 | return new Promise((resolve) => {
|
24 | setTimeout(resolve, ms);
|
25 | });
|
26 | }
|
27 | async 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 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | class 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 |
|
67 |
|
68 |
|
69 |
|
70 | async requestDeviceLogin() {
|
71 | const deviceFlowRequestUrl = this.getDeviceFlowRequestUrl();
|
72 | const loginOptions = this.getLoginOptions(deviceFlowRequestUrl);
|
73 | return makeRequest(loginOptions);
|
74 | }
|
75 | |
76 |
|
77 |
|
78 |
|
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 |
|
88 |
|
89 |
|
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 |
|
142 | const err = e.data;
|
143 | if (err.error && err.status === 400 && err.error === 'authorization_pending') {
|
144 |
|
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 |
|
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 | }
|
184 | exports.DeviceOauthService = DeviceOauthService;
|
185 | DeviceOauthService.RESPONSE_TYPE = 'device_code';
|
186 | DeviceOauthService.GRANT_TYPE = 'device';
|
187 | DeviceOauthService.SCOPE = 'refresh_token web api';
|
188 | DeviceOauthService.POLLING_COUNT_MAX = 100;
|
189 |
|
\ | No newline at end of file |