@salesforce/core 3.31.7 → 3.31.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +11 -11
- package/README.md +222 -222
- package/lib/config/aliasesConfig.d.ts +12 -12
- package/lib/config/aliasesConfig.js +27 -27
- package/lib/config/authInfoConfig.d.ts +19 -19
- package/lib/config/authInfoConfig.js +34 -34
- package/lib/config/config.d.ts +311 -311
- package/lib/config/config.js +574 -574
- package/lib/config/configAggregator.d.ts +232 -232
- package/lib/config/configAggregator.js +379 -379
- package/lib/config/configFile.d.ts +199 -199
- package/lib/config/configFile.js +340 -340
- package/lib/config/configGroup.d.ts +141 -141
- package/lib/config/configGroup.js +224 -224
- package/lib/config/configStore.d.ts +241 -241
- package/lib/config/configStore.js +352 -352
- package/lib/config/envVars.d.ts +101 -101
- package/lib/config/envVars.js +456 -456
- package/lib/config/orgUsersConfig.d.ts +31 -31
- package/lib/config/orgUsersConfig.js +41 -41
- package/lib/config/sandboxOrgConfig.d.ts +37 -37
- package/lib/config/sandboxOrgConfig.js +50 -50
- package/lib/config/sandboxProcessCache.d.ts +16 -16
- package/lib/config/sandboxProcessCache.js +37 -37
- package/lib/config/tokensConfig.d.ts +10 -10
- package/lib/config/tokensConfig.js +28 -28
- package/lib/config/ttlConfig.d.ts +34 -34
- package/lib/config/ttlConfig.js +54 -54
- package/lib/crypto/crypto.d.ts +54 -54
- package/lib/crypto/crypto.js +220 -220
- package/lib/crypto/keyChain.d.ts +8 -8
- package/lib/crypto/keyChain.js +61 -61
- package/lib/crypto/keyChainImpl.d.ts +116 -116
- package/lib/crypto/keyChainImpl.js +486 -486
- package/lib/crypto/secureBuffer.d.ts +46 -46
- package/lib/crypto/secureBuffer.js +82 -82
- package/lib/deviceOauthService.d.ts +71 -71
- package/lib/deviceOauthService.js +191 -191
- package/lib/exported.d.ts +38 -38
- package/lib/exported.js +118 -118
- package/lib/global.d.ts +70 -70
- package/lib/global.js +109 -109
- package/lib/lifecycleEvents.d.ts +93 -93
- package/lib/lifecycleEvents.js +188 -188
- package/lib/logger.d.ts +381 -381
- package/lib/logger.js +734 -734
- package/lib/messages.d.ts +291 -291
- package/lib/messages.js +543 -543
- package/lib/org/authInfo.d.ts +344 -344
- package/lib/org/authInfo.js +892 -892
- package/lib/org/authRemover.d.ts +88 -88
- package/lib/org/authRemover.js +182 -182
- package/lib/org/connection.d.ts +197 -197
- package/lib/org/connection.js +395 -395
- package/lib/org/index.d.ts +6 -6
- package/lib/org/index.js +28 -28
- package/lib/org/org.d.ts +558 -558
- package/lib/org/org.js +1267 -1267
- package/lib/org/orgConfigProperties.d.ts +69 -69
- package/lib/org/orgConfigProperties.js +136 -136
- package/lib/org/permissionSetAssignment.d.ts +35 -35
- package/lib/org/permissionSetAssignment.js +125 -125
- package/lib/org/scratchOrgCache.d.ts +20 -20
- package/lib/org/scratchOrgCache.js +32 -32
- package/lib/org/scratchOrgCreate.d.ts +54 -54
- package/lib/org/scratchOrgCreate.js +216 -216
- package/lib/org/scratchOrgErrorCodes.d.ts +10 -10
- package/lib/org/scratchOrgErrorCodes.js +88 -88
- package/lib/org/scratchOrgFeatureDeprecation.d.ts +26 -26
- package/lib/org/scratchOrgFeatureDeprecation.js +109 -109
- package/lib/org/scratchOrgInfoApi.d.ts +68 -68
- package/lib/org/scratchOrgInfoApi.js +413 -413
- package/lib/org/scratchOrgInfoGenerator.d.ts +64 -64
- package/lib/org/scratchOrgInfoGenerator.js +241 -241
- package/lib/org/scratchOrgLifecycleEvents.d.ts +10 -10
- package/lib/org/scratchOrgLifecycleEvents.js +40 -40
- package/lib/org/scratchOrgSettingsGenerator.d.ts +78 -78
- package/lib/org/scratchOrgSettingsGenerator.js +276 -276
- package/lib/org/scratchOrgTypes.d.ts +43 -43
- package/lib/org/scratchOrgTypes.js +8 -8
- package/lib/org/user.d.ts +187 -187
- package/lib/org/user.js +448 -448
- package/lib/schema/printer.d.ts +79 -79
- package/lib/schema/printer.js +260 -260
- package/lib/schema/validator.d.ts +70 -70
- package/lib/schema/validator.js +169 -169
- package/lib/sfError.d.ts +73 -73
- package/lib/sfError.js +136 -136
- package/lib/sfProject.d.ts +357 -357
- package/lib/sfProject.js +671 -671
- package/lib/stateAggregator/accessors/aliasAccessor.d.ts +98 -98
- package/lib/stateAggregator/accessors/aliasAccessor.js +145 -145
- package/lib/stateAggregator/accessors/orgAccessor.d.ts +101 -101
- package/lib/stateAggregator/accessors/orgAccessor.js +240 -240
- package/lib/stateAggregator/accessors/sandboxAccessor.d.ts +8 -8
- package/lib/stateAggregator/accessors/sandboxAccessor.js +27 -27
- package/lib/stateAggregator/accessors/tokenAccessor.d.ts +63 -63
- package/lib/stateAggregator/accessors/tokenAccessor.js +79 -79
- package/lib/stateAggregator/index.d.ts +4 -4
- package/lib/stateAggregator/index.js +26 -26
- package/lib/stateAggregator/stateAggregator.d.ts +25 -25
- package/lib/stateAggregator/stateAggregator.js +45 -45
- package/lib/status/myDomainResolver.d.ts +66 -66
- package/lib/status/myDomainResolver.js +124 -124
- package/lib/status/pollingClient.d.ts +85 -85
- package/lib/status/pollingClient.js +115 -115
- package/lib/status/streamingClient.d.ts +244 -244
- package/lib/status/streamingClient.js +436 -436
- package/lib/status/types.d.ts +89 -89
- package/lib/status/types.js +17 -17
- package/lib/testSetup.d.ts +553 -553
- package/lib/testSetup.js +871 -871
- package/lib/util/cache.d.ts +11 -11
- package/lib/util/cache.js +69 -69
- package/lib/util/checkLightningDomain.d.ts +1 -1
- package/lib/util/checkLightningDomain.js +28 -28
- package/lib/util/directoryWriter.d.ts +12 -12
- package/lib/util/directoryWriter.js +53 -53
- package/lib/util/getJwtAudienceUrl.d.ts +4 -4
- package/lib/util/getJwtAudienceUrl.js +18 -18
- package/lib/util/internal.d.ts +58 -58
- package/lib/util/internal.js +118 -118
- package/lib/util/jsonXmlTools.d.ts +14 -14
- package/lib/util/jsonXmlTools.js +38 -38
- package/lib/util/mapKeys.d.ts +14 -14
- package/lib/util/mapKeys.js +51 -51
- package/lib/util/sfdc.d.ts +52 -52
- package/lib/util/sfdc.js +85 -85
- package/lib/util/sfdcUrl.d.ts +72 -72
- package/lib/util/sfdcUrl.js +215 -215
- package/lib/util/structuredWriter.d.ts +9 -9
- package/lib/util/structuredWriter.js +2 -2
- package/lib/util/zipWriter.d.ts +16 -16
- package/lib/util/zipWriter.js +67 -67
- package/lib/webOAuthServer.d.ts +156 -156
- package/lib/webOAuthServer.js +388 -388
- package/messages/auth.md +37 -37
- package/messages/config.md +156 -156
- package/messages/connection.md +30 -30
- package/messages/core.json +20 -20
- package/messages/core.md +67 -67
- package/messages/encryption.md +85 -85
- package/messages/envVars.md +303 -303
- package/messages/org.md +63 -63
- package/messages/permissionSetAssignment.md +31 -31
- package/messages/scratchOrgCreate.md +23 -23
- package/messages/scratchOrgErrorCodes.md +115 -115
- package/messages/scratchOrgFeatureDeprecation.md +11 -11
- package/messages/scratchOrgInfoApi.md +15 -15
- package/messages/scratchOrgInfoGenerator.md +23 -23
- package/messages/streaming.md +23 -23
- package/messages/user.md +35 -35
- package/package.json +97 -97
package/lib/org/authInfo.js
CHANGED
|
@@ -1,893 +1,893 @@
|
|
|
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 class-methods-use-this */
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.AuthInfo = exports.DEFAULT_CONNECTED_APP_INFO = void 0;
|
|
11
|
-
const crypto_1 = require("crypto");
|
|
12
|
-
const path_1 = require("path");
|
|
13
|
-
const os = require("os");
|
|
14
|
-
const fs = require("fs");
|
|
15
|
-
const kit_1 = require("@salesforce/kit");
|
|
16
|
-
const ts_types_1 = require("@salesforce/ts-types");
|
|
17
|
-
const jsforce_1 = require("jsforce");
|
|
18
|
-
const transport_1 = require("jsforce/lib/transport");
|
|
19
|
-
const jwt = require("jsonwebtoken");
|
|
20
|
-
const config_1 = require("../config/config");
|
|
21
|
-
const configAggregator_1 = require("../config/configAggregator");
|
|
22
|
-
const logger_1 = require("../logger");
|
|
23
|
-
const sfError_1 = require("../sfError");
|
|
24
|
-
const sfdc_1 = require("../util/sfdc");
|
|
25
|
-
const stateAggregator_1 = require("../stateAggregator");
|
|
26
|
-
const messages_1 = require("../messages");
|
|
27
|
-
const sfdcUrl_1 = require("../util/sfdcUrl");
|
|
28
|
-
const connection_1 = require("./connection");
|
|
29
|
-
const orgConfigProperties_1 = require("./orgConfigProperties");
|
|
30
|
-
const org_1 = require("./org");
|
|
31
|
-
messages_1.Messages.importMessagesDirectory(__dirname);
|
|
32
|
-
const messages = messages_1.Messages.load('@salesforce/core', 'core', [
|
|
33
|
-
'authInfoCreationError',
|
|
34
|
-
'authInfoOverwriteError',
|
|
35
|
-
'namedOrgNotFound',
|
|
36
|
-
'orgDataNotAvailableError',
|
|
37
|
-
'orgDataNotAvailableError.actions',
|
|
38
|
-
'refreshTokenAuthError',
|
|
39
|
-
'jwtAuthErrors',
|
|
40
|
-
'authCodeUsernameRetrievalError',
|
|
41
|
-
'authCodeExchangeError',
|
|
42
|
-
'missingClientId',
|
|
43
|
-
]);
|
|
44
|
-
// parses the id field returned from jsForce oauth2 methods to get
|
|
45
|
-
// user ID and org ID.
|
|
46
|
-
function parseIdUrl(idUrl) {
|
|
47
|
-
const idUrls = idUrl.split('/');
|
|
48
|
-
const userId = idUrls.pop();
|
|
49
|
-
const orgId = idUrls.pop();
|
|
50
|
-
return {
|
|
51
|
-
userId,
|
|
52
|
-
orgId,
|
|
53
|
-
url: idUrl,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
exports.DEFAULT_CONNECTED_APP_INFO = {
|
|
57
|
-
clientId: 'PlatformCLI',
|
|
58
|
-
// Legacy. The connected app info is owned by the thing that
|
|
59
|
-
// creates new AuthInfos. Currently that is the auth:* commands which
|
|
60
|
-
// aren't owned by this core library. These values need to be here
|
|
61
|
-
// for any old auth files where the id and secret aren't stored.
|
|
62
|
-
//
|
|
63
|
-
// Ideally, this would be removed at some point in the distant future
|
|
64
|
-
// when all auth files now have the clientId stored in it.
|
|
65
|
-
legacyClientId: 'SalesforceDevelopmentExperience',
|
|
66
|
-
legacyClientSecret: '1384510088588713504',
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* Handles persistence and fetching of user authentication information using
|
|
70
|
-
* JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
|
|
71
|
-
* use to keep tokens active. An AuthInfo can also be created with an access
|
|
72
|
-
* token, but AuthInfos created with access tokens can't be persisted to disk.
|
|
73
|
-
*
|
|
74
|
-
* **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm)
|
|
75
|
-
*
|
|
76
|
-
* **See** [Salesforce DX Usernames and Orgs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm)
|
|
77
|
-
*
|
|
78
|
-
* ```
|
|
79
|
-
* // Creating a new authentication file.
|
|
80
|
-
* const authInfo = await AuthInfo.create({
|
|
81
|
-
* username: myAdminUsername,
|
|
82
|
-
* oauth2Options: {
|
|
83
|
-
* loginUrl, authCode, clientId, clientSecret
|
|
84
|
-
* }
|
|
85
|
-
* );
|
|
86
|
-
* authInfo.save();
|
|
87
|
-
*
|
|
88
|
-
* // Creating an authorization info with an access token.
|
|
89
|
-
* const authInfo = await AuthInfo.create({
|
|
90
|
-
* username: accessToken
|
|
91
|
-
* });
|
|
92
|
-
*
|
|
93
|
-
* // Using an existing authentication file.
|
|
94
|
-
* const authInfo = await AuthInfo.create({
|
|
95
|
-
* username: myAdminUsername
|
|
96
|
-
* });
|
|
97
|
-
*
|
|
98
|
-
* // Using the AuthInfo
|
|
99
|
-
* const connection = await Connection.create({ authInfo });
|
|
100
|
-
* ```
|
|
101
|
-
*/
|
|
102
|
-
class AuthInfo extends kit_1.AsyncOptionalCreatable {
|
|
103
|
-
/**
|
|
104
|
-
* Constructor
|
|
105
|
-
* **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.**
|
|
106
|
-
*
|
|
107
|
-
* @param options The options for the class instance
|
|
108
|
-
*/
|
|
109
|
-
constructor(options) {
|
|
110
|
-
super(options);
|
|
111
|
-
// Possibly overridden in create
|
|
112
|
-
this.usingAccessToken = false;
|
|
113
|
-
this.options = options ?? {};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Returns the default instance url
|
|
117
|
-
*
|
|
118
|
-
* @returns {string}
|
|
119
|
-
*/
|
|
120
|
-
static getDefaultInstanceUrl() {
|
|
121
|
-
const configuredInstanceUrl = configAggregator_1.ConfigAggregator.getValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL).value;
|
|
122
|
-
return configuredInstanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Get a list of all authorizations based on auth files stored in the global directory.
|
|
126
|
-
* One can supply a filter (see @param orgAuthFilter) and calling this function without
|
|
127
|
-
* a filter will return all authorizations.
|
|
128
|
-
*
|
|
129
|
-
* @param orgAuthFilter A predicate function that returns true for those org authorizations that are to be retained.
|
|
130
|
-
*
|
|
131
|
-
* @returns {Promise<OrgAuthorization[]>}
|
|
132
|
-
*/
|
|
133
|
-
static async listAllAuthorizations(orgAuthFilter = (orgAuth) => !!orgAuth) {
|
|
134
|
-
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
|
|
135
|
-
const config = (await configAggregator_1.ConfigAggregator.create()).getConfigInfo();
|
|
136
|
-
const orgs = await stateAggregator.orgs.readAll();
|
|
137
|
-
const final = [];
|
|
138
|
-
for (const org of orgs) {
|
|
139
|
-
const username = (0, ts_types_1.ensureString)(org.username);
|
|
140
|
-
const aliases = stateAggregator.aliases.getAll(username) ?? undefined;
|
|
141
|
-
// Get a list of configuration values that are set to either the username or one
|
|
142
|
-
// of the aliases
|
|
143
|
-
const configs = config
|
|
144
|
-
.filter((c) => aliases.includes(c.value) || c.value === username)
|
|
145
|
-
.map((c) => c.key);
|
|
146
|
-
try {
|
|
147
|
-
// prevent ConfigFile collision bug
|
|
148
|
-
// eslint-disable-next-line no-await-in-loop
|
|
149
|
-
const authInfo = await AuthInfo.create({ username });
|
|
150
|
-
const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authInfo.getFields();
|
|
151
|
-
final.push({
|
|
152
|
-
aliases,
|
|
153
|
-
configs,
|
|
154
|
-
username,
|
|
155
|
-
instanceUrl,
|
|
156
|
-
isScratchOrg: Boolean(devHubUsername),
|
|
157
|
-
isDevHub: isDevHub ?? false,
|
|
158
|
-
// eslint-disable-next-line no-await-in-loop
|
|
159
|
-
isSandbox: await stateAggregator.sandboxes.hasFile(orgId),
|
|
160
|
-
orgId: orgId,
|
|
161
|
-
accessToken: authInfo.getConnectionOptions().accessToken,
|
|
162
|
-
oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
|
|
163
|
-
isExpired: Boolean(devHubUsername) && expirationDate
|
|
164
|
-
? new Date((0, ts_types_1.ensureString)(expirationDate)).getTime() < new Date().getTime()
|
|
165
|
-
: 'unknown',
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
catch (err) {
|
|
169
|
-
final.push({
|
|
170
|
-
aliases,
|
|
171
|
-
configs,
|
|
172
|
-
username,
|
|
173
|
-
orgId: org.orgId,
|
|
174
|
-
instanceUrl: org.instanceUrl,
|
|
175
|
-
accessToken: undefined,
|
|
176
|
-
oauthMethod: 'unknown',
|
|
177
|
-
error: err.message,
|
|
178
|
-
isExpired: 'unknown',
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return final.filter(orgAuthFilter);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Returns true if one or more authentications are persisted.
|
|
186
|
-
*/
|
|
187
|
-
static async hasAuthentications() {
|
|
188
|
-
try {
|
|
189
|
-
const auths = await (await stateAggregator_1.StateAggregator.getInstance()).orgs.list();
|
|
190
|
-
return !(0, kit_1.isEmpty)(auths);
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
const error = err;
|
|
194
|
-
if (error.name === 'OrgDataNotAvailableError' || error.code === 'ENOENT') {
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Get the authorization URL.
|
|
202
|
-
*
|
|
203
|
-
* @param options The options to generate the URL.
|
|
204
|
-
*/
|
|
205
|
-
static getAuthorizationUrl(options, oauth2) {
|
|
206
|
-
// Always use a verifier for enhanced security
|
|
207
|
-
options.useVerifier = true;
|
|
208
|
-
const oauth2Verifier = oauth2 ?? new jsforce_1.OAuth2(options);
|
|
209
|
-
// The state parameter allows the redirectUri callback listener to ignore request
|
|
210
|
-
// that don't contain the state value.
|
|
211
|
-
const params = {
|
|
212
|
-
state: (0, crypto_1.randomBytes)(Math.ceil(6)).toString('hex'),
|
|
213
|
-
prompt: 'login',
|
|
214
|
-
// Default connected app is 'refresh_token api web'
|
|
215
|
-
scope: options.scope ?? kit_1.env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'),
|
|
216
|
-
};
|
|
217
|
-
return oauth2Verifier.getAuthorizationUrl(params);
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`.
|
|
221
|
-
*
|
|
222
|
-
* @example
|
|
223
|
-
* ```
|
|
224
|
-
* await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl));
|
|
225
|
-
* ```
|
|
226
|
-
* @param sfdxAuthUrl
|
|
227
|
-
*/
|
|
228
|
-
static parseSfdxAuthUrl(sfdxAuthUrl) {
|
|
229
|
-
const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)/);
|
|
230
|
-
if (!match) {
|
|
231
|
-
throw new sfError_1.SfError('Invalid SFDX auth URL. Must be in the format "force://<clientId>:<clientSecret>:<refreshToken>@<instanceUrl>". Note that the SFDX auth URL uses the "force" protocol, and not "http" or "https". Also note that the "instanceUrl" inside the SFDX auth URL doesn\'t include the protocol ("https://").', 'INVALID_SFDX_AUTH_URL');
|
|
232
|
-
}
|
|
233
|
-
const [, clientId, clientSecret, refreshToken, loginUrl] = match;
|
|
234
|
-
return {
|
|
235
|
-
clientId,
|
|
236
|
-
clientSecret,
|
|
237
|
-
refreshToken,
|
|
238
|
-
loginUrl: `https://${loginUrl}`,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Given a set of decrypted fields and an authInfo, determine if the org belongs to an available
|
|
243
|
-
* dev hub.
|
|
244
|
-
*
|
|
245
|
-
* @param fields
|
|
246
|
-
* @param orgAuthInfo
|
|
247
|
-
*/
|
|
248
|
-
static async identifyPossibleScratchOrgs(fields, orgAuthInfo) {
|
|
249
|
-
// fields property is passed in because the consumers of this method have performed the decrypt.
|
|
250
|
-
// This is so we don't have to call authInfo.getFields(true) and decrypt again OR accidentally save an
|
|
251
|
-
// authInfo before it is necessary.
|
|
252
|
-
const logger = await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' });
|
|
253
|
-
// return if we already know the hub org we know it is a devhub or prod-like or no orgId present
|
|
254
|
-
if (fields.isDevHub || fields.devHubUsername || !fields.orgId)
|
|
255
|
-
return;
|
|
256
|
-
logger.debug('getting devHubs');
|
|
257
|
-
// TODO: return if url is not sandbox-like to avoid constantly asking about production orgs
|
|
258
|
-
// TODO: someday we make this easier by asking the org if it is a scratch org
|
|
259
|
-
const hubAuthInfos = await AuthInfo.getDevHubAuthInfos();
|
|
260
|
-
logger.debug(`found ${hubAuthInfos.length} DevHubs`);
|
|
261
|
-
if (hubAuthInfos.length === 0)
|
|
262
|
-
return;
|
|
263
|
-
// ask all those orgs if they know this orgId
|
|
264
|
-
await Promise.all(hubAuthInfos.map(async (hubAuthInfo) => {
|
|
265
|
-
try {
|
|
266
|
-
const data = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId);
|
|
267
|
-
if (data.totalSize > 0) {
|
|
268
|
-
// if any return a result
|
|
269
|
-
logger.debug(`found orgId ${fields.orgId} in devhub ${hubAuthInfo.username}`);
|
|
270
|
-
try {
|
|
271
|
-
await orgAuthInfo.save({ ...fields, devHubUsername: hubAuthInfo.username });
|
|
272
|
-
logger.debug(`set ${hubAuthInfo.username} as devhub for scratch org ${orgAuthInfo.getUsername()}`);
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error);
|
|
281
|
-
}
|
|
282
|
-
}));
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Find all dev hubs available in the local environment.
|
|
286
|
-
*/
|
|
287
|
-
static async getDevHubAuthInfos() {
|
|
288
|
-
return AuthInfo.listAllAuthorizations((possibleHub) => possibleHub?.isDevHub ?? false);
|
|
289
|
-
}
|
|
290
|
-
static async queryScratchOrg(devHubUsername, scratchOrgId) {
|
|
291
|
-
const devHubOrg = await org_1.Org.create({ aliasOrUsername: devHubUsername });
|
|
292
|
-
const conn = devHubOrg.getConnection();
|
|
293
|
-
const data = await conn.query(`select Id from ScratchOrgInfo where ScratchOrg = '${sfdc_1.sfdc.trimTo15(scratchOrgId)}'`);
|
|
294
|
-
return data;
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Get the username.
|
|
298
|
-
*/
|
|
299
|
-
getUsername() {
|
|
300
|
-
return this.username;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Returns true if `this` is using the JWT flow.
|
|
304
|
-
*/
|
|
305
|
-
isJwt() {
|
|
306
|
-
const { refreshToken, privateKey } = this.getFields();
|
|
307
|
-
return !refreshToken && !!privateKey;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Returns true if `this` is using an access token flow.
|
|
311
|
-
*/
|
|
312
|
-
isAccessTokenFlow() {
|
|
313
|
-
const { refreshToken, privateKey } = this.getFields();
|
|
314
|
-
return !refreshToken && !privateKey;
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Returns true if `this` is using the oauth flow.
|
|
318
|
-
*/
|
|
319
|
-
isOauth() {
|
|
320
|
-
return !this.isAccessTokenFlow() && !this.isJwt();
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Returns true if `this` is using the refresh token flow.
|
|
324
|
-
*/
|
|
325
|
-
isRefreshTokenFlow() {
|
|
326
|
-
const { refreshToken, authCode } = this.getFields();
|
|
327
|
-
return !authCode && !!refreshToken;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Updates the cache and persists the authentication fields (encrypted).
|
|
331
|
-
*
|
|
332
|
-
* @param authData New data to save.
|
|
333
|
-
*/
|
|
334
|
-
async save(authData) {
|
|
335
|
-
this.update(authData);
|
|
336
|
-
const username = (0, ts_types_1.ensure)(this.getUsername());
|
|
337
|
-
if (sfdc_1.sfdc.matchesAccessToken(username)) {
|
|
338
|
-
this.logger.debug('Username is an accesstoken. Skip saving authinfo to disk.');
|
|
339
|
-
return this;
|
|
340
|
-
}
|
|
341
|
-
await this.stateAggregator.orgs.write(username);
|
|
342
|
-
this.logger.info(`Saved auth info for username: ${username}`);
|
|
343
|
-
return this;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Update the authorization fields, encrypting sensitive fields, but do not persist.
|
|
347
|
-
* For convenience `this` object is returned.
|
|
348
|
-
*
|
|
349
|
-
* @param authData Authorization fields to update.
|
|
350
|
-
*/
|
|
351
|
-
update(authData) {
|
|
352
|
-
if (authData && (0, ts_types_1.isPlainObject)(authData)) {
|
|
353
|
-
this.username = authData.username ?? this.username;
|
|
354
|
-
this.stateAggregator.orgs.update(this.username, authData);
|
|
355
|
-
this.logger.info(`Updated auth info for username: ${this.username}`);
|
|
356
|
-
}
|
|
357
|
-
return this;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Get the auth fields (decrypted) needed to make a connection.
|
|
361
|
-
*/
|
|
362
|
-
getConnectionOptions() {
|
|
363
|
-
let opts;
|
|
364
|
-
const decryptedCopy = this.getFields(true);
|
|
365
|
-
const { accessToken, instanceUrl, loginUrl } = decryptedCopy;
|
|
366
|
-
if (this.isAccessTokenFlow()) {
|
|
367
|
-
this.logger.info('Returning fields for a connection using access token.');
|
|
368
|
-
// Just auth with the accessToken
|
|
369
|
-
opts = { accessToken, instanceUrl, loginUrl };
|
|
370
|
-
}
|
|
371
|
-
else if (this.isJwt()) {
|
|
372
|
-
this.logger.info('Returning fields for a connection using JWT config.');
|
|
373
|
-
opts = {
|
|
374
|
-
accessToken,
|
|
375
|
-
instanceUrl,
|
|
376
|
-
refreshFn: this.refreshFn.bind(this),
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
// @TODO: figure out loginUrl and redirectUri (probably get from config class)
|
|
381
|
-
//
|
|
382
|
-
// redirectUri: org.config.getOauthCallbackUrl()
|
|
383
|
-
// loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl
|
|
384
|
-
this.logger.info('Returning fields for a connection using OAuth config.');
|
|
385
|
-
// Decrypt a user provided client secret or use the default.
|
|
386
|
-
opts = {
|
|
387
|
-
oauth2: {
|
|
388
|
-
loginUrl: instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION,
|
|
389
|
-
clientId: this.getClientId(),
|
|
390
|
-
redirectUri: this.getRedirectUri(),
|
|
391
|
-
},
|
|
392
|
-
accessToken,
|
|
393
|
-
instanceUrl,
|
|
394
|
-
refreshFn: this.refreshFn.bind(this),
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
// decrypt the fields
|
|
398
|
-
return opts;
|
|
399
|
-
}
|
|
400
|
-
getClientId() {
|
|
401
|
-
return this.getFields()?.clientId ?? exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
|
|
402
|
-
}
|
|
403
|
-
getRedirectUri() {
|
|
404
|
-
return 'http://localhost:1717/OauthRedirect';
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Get the authorization fields.
|
|
408
|
-
*
|
|
409
|
-
* @param decrypt Decrypt the fields.
|
|
410
|
-
*/
|
|
411
|
-
getFields(decrypt) {
|
|
412
|
-
return this.stateAggregator.orgs.get(this.username, decrypt) ?? {};
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Get the org front door (used for web based oauth flows)
|
|
416
|
-
*/
|
|
417
|
-
getOrgFrontDoorUrl() {
|
|
418
|
-
const authFields = this.getFields(true);
|
|
419
|
-
const base = (0, ts_types_1.ensureString)(authFields.instanceUrl).replace(/\/+$/, '');
|
|
420
|
-
const accessToken = (0, ts_types_1.ensureString)(authFields.accessToken);
|
|
421
|
-
return `${base}/secur/frontdoor.jsp?sid=${accessToken}`;
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Returns true if this org is using access token auth.
|
|
425
|
-
*/
|
|
426
|
-
isUsingAccessToken() {
|
|
427
|
-
return this.usingAccessToken;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Get the SFDX Auth URL.
|
|
431
|
-
*
|
|
432
|
-
* **See** [SFDX Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_auth.htm#cli_reference_force_auth)
|
|
433
|
-
*/
|
|
434
|
-
getSfdxAuthUrl() {
|
|
435
|
-
const decryptedFields = this.getFields(true);
|
|
436
|
-
const instanceUrl = (0, ts_types_1.ensure)(decryptedFields.instanceUrl, 'undefined instanceUrl').replace(/^https?:\/\//, '');
|
|
437
|
-
let sfdxAuthUrl = 'force://';
|
|
438
|
-
if (decryptedFields.clientId) {
|
|
439
|
-
sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret ?? ''}:`;
|
|
440
|
-
}
|
|
441
|
-
sfdxAuthUrl += `${(0, ts_types_1.ensure)(decryptedFields.refreshToken, 'undefined refreshToken')}@${instanceUrl}`;
|
|
442
|
-
return sfdxAuthUrl;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Convenience function to handle typical side effects encountered when dealing with an AuthInfo.
|
|
446
|
-
* Given the values supplied in parameter sideEffects, this function will set auth alias, default auth
|
|
447
|
-
* and default dev hub.
|
|
448
|
-
*
|
|
449
|
-
* @param sideEffects - instance of AuthSideEffects
|
|
450
|
-
*/
|
|
451
|
-
async handleAliasAndDefaultSettings(sideEffects) {
|
|
452
|
-
if (sideEffects.alias ||
|
|
453
|
-
sideEffects.setDefault ||
|
|
454
|
-
sideEffects.setDefaultDevHub ||
|
|
455
|
-
typeof sideEffects.setTracksSource === 'boolean') {
|
|
456
|
-
if (sideEffects.alias)
|
|
457
|
-
await this.setAlias(sideEffects.alias);
|
|
458
|
-
if (sideEffects.setDefault)
|
|
459
|
-
await this.setAsDefault({ org: true });
|
|
460
|
-
if (sideEffects.setDefaultDevHub)
|
|
461
|
-
await this.setAsDefault({ devHub: true });
|
|
462
|
-
if (typeof sideEffects.setTracksSource === 'boolean') {
|
|
463
|
-
await this.save({ tracksSource: sideEffects.setTracksSource });
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
await this.save();
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Set the target-env (default) or the target-dev-hub to the alias if
|
|
472
|
-
* it exists otherwise to the username. Method will try to set the local
|
|
473
|
-
* config first but will default to global config if that fails.
|
|
474
|
-
*
|
|
475
|
-
* @param options
|
|
476
|
-
*/
|
|
477
|
-
async setAsDefault(options = { org: true }) {
|
|
478
|
-
let config;
|
|
479
|
-
// if we fail to create the local config, default to the global config
|
|
480
|
-
try {
|
|
481
|
-
config = await config_1.Config.create({ isGlobal: false });
|
|
482
|
-
}
|
|
483
|
-
catch {
|
|
484
|
-
config = await config_1.Config.create({ isGlobal: true });
|
|
485
|
-
}
|
|
486
|
-
const username = (0, ts_types_1.ensureString)(this.getUsername());
|
|
487
|
-
const alias = this.stateAggregator.aliases.get(username);
|
|
488
|
-
const value = alias ?? username;
|
|
489
|
-
if (options.org) {
|
|
490
|
-
config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG, value);
|
|
491
|
-
}
|
|
492
|
-
if (options.devHub) {
|
|
493
|
-
config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB, value);
|
|
494
|
-
}
|
|
495
|
-
await config.write();
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Sets the provided alias to the username
|
|
499
|
-
*
|
|
500
|
-
* @param alias alias to set
|
|
501
|
-
*/
|
|
502
|
-
async setAlias(alias) {
|
|
503
|
-
this.stateAggregator.aliases.set(alias, this.getUsername());
|
|
504
|
-
await this.stateAggregator.aliases.write();
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Initializes an instance of the AuthInfo class.
|
|
508
|
-
*/
|
|
509
|
-
async init() {
|
|
510
|
-
this.stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
|
|
511
|
-
const username = this.options.username;
|
|
512
|
-
const authOptions = this.options.oauth2Options ?? this.options.accessTokenOptions;
|
|
513
|
-
// Must specify either username and/or options
|
|
514
|
-
if (!username && !authOptions) {
|
|
515
|
-
throw messages.createError('authInfoCreationError');
|
|
516
|
-
}
|
|
517
|
-
// If a username AND oauth options, ensure an authorization for the username doesn't
|
|
518
|
-
// already exist. Throw if it does so we don't overwrite the authorization.
|
|
519
|
-
if (username && authOptions) {
|
|
520
|
-
if (await this.stateAggregator.orgs.hasFile(username)) {
|
|
521
|
-
throw messages.createError('authInfoOverwriteError');
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const oauthUsername = username ?? authOptions?.username;
|
|
525
|
-
if (oauthUsername) {
|
|
526
|
-
this.username = oauthUsername;
|
|
527
|
-
await this.stateAggregator.orgs.read(oauthUsername, false, false);
|
|
528
|
-
} // Else it will be set in initAuthOptions below.
|
|
529
|
-
// If the username is an access token, use that for auth and don't persist
|
|
530
|
-
if ((0, ts_types_1.isString)(oauthUsername) && sfdc_1.sfdc.matchesAccessToken(oauthUsername)) {
|
|
531
|
-
// Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
|
|
532
|
-
this.logger = await logger_1.Logger.child('AuthInfo');
|
|
533
|
-
const aggregator = await configAggregator_1.ConfigAggregator.create();
|
|
534
|
-
const instanceUrl = this.getInstanceUrl(aggregator, authOptions);
|
|
535
|
-
this.update({
|
|
536
|
-
accessToken: oauthUsername,
|
|
537
|
-
instanceUrl,
|
|
538
|
-
orgId: oauthUsername.split('!')[0],
|
|
539
|
-
loginUrl: instanceUrl,
|
|
540
|
-
});
|
|
541
|
-
this.usingAccessToken = true;
|
|
542
|
-
}
|
|
543
|
-
// If a username with NO oauth options, ensure authorization already exist.
|
|
544
|
-
else if (username && !authOptions && !(await this.stateAggregator.orgs.exists(username))) {
|
|
545
|
-
throw messages.createError('namedOrgNotFound', [username]);
|
|
546
|
-
}
|
|
547
|
-
else {
|
|
548
|
-
await this.initAuthOptions(authOptions);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
getInstanceUrl(aggregator, options) {
|
|
552
|
-
const instanceUrl = options?.instanceUrl ?? aggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL);
|
|
553
|
-
return instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache
|
|
557
|
-
* or by reading from the persistence store. For convenience `this` object is returned.
|
|
558
|
-
*
|
|
559
|
-
* @param options Options to be used for creating an OAuth2 instance.
|
|
560
|
-
*
|
|
561
|
-
* **Throws** *{@link SfError}{ name: 'NamedOrgNotFoundError' }* Org information does not exist.
|
|
562
|
-
* @returns {Promise<AuthInfo>}
|
|
563
|
-
*/
|
|
564
|
-
async initAuthOptions(options) {
|
|
565
|
-
this.logger = await logger_1.Logger.child('AuthInfo');
|
|
566
|
-
// If options were passed, use those before checking cache and reading an auth file.
|
|
567
|
-
let authConfig;
|
|
568
|
-
if (options) {
|
|
569
|
-
options = (0, kit_1.cloneJson)(options);
|
|
570
|
-
if (this.isTokenOptions(options)) {
|
|
571
|
-
authConfig = options;
|
|
572
|
-
const userInfo = await this.retrieveUserInfo((0, ts_types_1.ensureString)(options.instanceUrl), (0, ts_types_1.ensureString)(options.accessToken));
|
|
573
|
-
this.update({ username: userInfo?.username, orgId: userInfo?.organizationId });
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
if (this.options.parentUsername) {
|
|
577
|
-
const parentFields = await this.loadDecryptedAuthFromConfig(this.options.parentUsername);
|
|
578
|
-
options.clientId = parentFields.clientId;
|
|
579
|
-
if (process.env.SFDX_CLIENT_SECRET) {
|
|
580
|
-
options.clientSecret = process.env.SFDX_CLIENT_SECRET;
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
// Grab whatever flow is defined
|
|
584
|
-
Object.assign(options, {
|
|
585
|
-
clientSecret: parentFields.clientSecret,
|
|
586
|
-
privateKey: parentFields.privateKey ? (0, path_1.resolve)(parentFields.privateKey) : parentFields.privateKey,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
// jwt flow
|
|
591
|
-
// Support both sfdx and jsforce private key values
|
|
592
|
-
if (!options.privateKey && options.privateKeyFile) {
|
|
593
|
-
options.privateKey = (0, path_1.resolve)(options.privateKeyFile);
|
|
594
|
-
}
|
|
595
|
-
if (options.privateKey) {
|
|
596
|
-
authConfig = await this.authJwt(options);
|
|
597
|
-
}
|
|
598
|
-
else if (!options.authCode && options.refreshToken) {
|
|
599
|
-
// refresh token flow (from sfdxUrl or OAuth refreshFn)
|
|
600
|
-
authConfig = await this.buildRefreshTokenConfig(options);
|
|
601
|
-
}
|
|
602
|
-
else if (this.options.oauth2 instanceof jsforce_1.OAuth2) {
|
|
603
|
-
// authcode exchange / web auth flow
|
|
604
|
-
authConfig = await this.exchangeToken(options, this.options.oauth2);
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
authConfig = await this.exchangeToken(options);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
authConfig.isDevHub = await this.determineIfDevHub((0, ts_types_1.ensureString)(authConfig.instanceUrl), (0, ts_types_1.ensureString)(authConfig.accessToken));
|
|
611
|
-
if (authConfig.username)
|
|
612
|
-
await this.stateAggregator.orgs.read(authConfig.username, false, false);
|
|
613
|
-
// Update the auth fields WITH encryption
|
|
614
|
-
this.update(authConfig);
|
|
615
|
-
}
|
|
616
|
-
return this;
|
|
617
|
-
}
|
|
618
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
619
|
-
async loadDecryptedAuthFromConfig(username) {
|
|
620
|
-
// Fetch from the persisted auth file
|
|
621
|
-
const authInfo = this.stateAggregator.orgs.get(username, true);
|
|
622
|
-
if (!authInfo) {
|
|
623
|
-
throw messages.createError('namedOrgNotFound', [username]);
|
|
624
|
-
}
|
|
625
|
-
return authInfo;
|
|
626
|
-
}
|
|
627
|
-
isTokenOptions(options) {
|
|
628
|
-
// Although OAuth2Config does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in
|
|
629
|
-
// which WILL have an access token as well, but it should be considered an OAuth2Config at that point.
|
|
630
|
-
return ('accessToken' in options &&
|
|
631
|
-
!('refreshToken' in options) &&
|
|
632
|
-
!('privateKey' in options) &&
|
|
633
|
-
!('privateKeyFile' in options) &&
|
|
634
|
-
!('authCode' in options));
|
|
635
|
-
}
|
|
636
|
-
// A callback function for a connection to refresh an access token. This is used
|
|
637
|
-
// both for a JWT connection and an OAuth connection.
|
|
638
|
-
async refreshFn(conn, callback) {
|
|
639
|
-
this.logger.info('Access token has expired. Updating...');
|
|
640
|
-
try {
|
|
641
|
-
const fields = this.getFields(true);
|
|
642
|
-
await this.initAuthOptions(fields);
|
|
643
|
-
await this.save();
|
|
644
|
-
return await callback(null, fields.accessToken);
|
|
645
|
-
}
|
|
646
|
-
catch (err) {
|
|
647
|
-
const error = err;
|
|
648
|
-
if (error?.message?.includes('Data Not Available')) {
|
|
649
|
-
// Set cause to keep original stacktrace
|
|
650
|
-
return await callback(messages.createError('orgDataNotAvailableError', [this.getUsername()], [], error));
|
|
651
|
-
}
|
|
652
|
-
return await callback(error);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
async readJwtKey(keyFile) {
|
|
656
|
-
return fs.promises.readFile(keyFile, 'utf8');
|
|
657
|
-
}
|
|
658
|
-
// Build OAuth config for a JWT auth flow
|
|
659
|
-
async authJwt(options) {
|
|
660
|
-
if (!options.clientId) {
|
|
661
|
-
throw messages.createError('missingClientId');
|
|
662
|
-
}
|
|
663
|
-
const privateKeyContents = await this.readJwtKey((0, ts_types_1.ensureString)(options.privateKey));
|
|
664
|
-
const { loginUrl = sfdcUrl_1.SfdcUrl.PRODUCTION } = options;
|
|
665
|
-
const url = new sfdcUrl_1.SfdcUrl(loginUrl);
|
|
666
|
-
const createdOrgInstance = (this.getFields().createdOrgInstance ?? '').trim().toLowerCase();
|
|
667
|
-
const audienceUrl = await url.getJwtAudienceUrl(createdOrgInstance);
|
|
668
|
-
let authFieldsBuilder;
|
|
669
|
-
const authErrors = [];
|
|
670
|
-
// given that we can no longer depend on instance names or URls to determine audience, let's try them all
|
|
671
|
-
const loginAndAudienceUrls = (0, sfdcUrl_1.getLoginAudienceCombos)(audienceUrl, loginUrl);
|
|
672
|
-
for (const [login, audience] of loginAndAudienceUrls) {
|
|
673
|
-
try {
|
|
674
|
-
// sequentially, in probabilistic order
|
|
675
|
-
// eslint-disable-next-line no-await-in-loop
|
|
676
|
-
authFieldsBuilder = await this.tryJwtAuth(options.clientId, login, audience, privateKeyContents);
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
catch (err) {
|
|
680
|
-
const error = err;
|
|
681
|
-
const message = error.message.includes('audience')
|
|
682
|
-
? `${error.message} [audience=${audience} login=${login}]`
|
|
683
|
-
: error.message;
|
|
684
|
-
authErrors.push(message);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (!authFieldsBuilder) {
|
|
688
|
-
// messages.createError expects names to end in `error` and this one says Errors so do it manually.
|
|
689
|
-
throw new sfError_1.SfError(messages.getMessage('jwtAuthErrors', [authErrors.join('\n')]), 'JwtAuthError');
|
|
690
|
-
}
|
|
691
|
-
const authFields = {
|
|
692
|
-
accessToken: (0, ts_types_1.asString)(authFieldsBuilder.access_token),
|
|
693
|
-
orgId: parseIdUrl((0, ts_types_1.ensureString)(authFieldsBuilder.id)).orgId,
|
|
694
|
-
loginUrl: options.loginUrl,
|
|
695
|
-
privateKey: options.privateKey,
|
|
696
|
-
clientId: options.clientId,
|
|
697
|
-
};
|
|
698
|
-
const instanceUrl = (0, ts_types_1.ensureString)(authFieldsBuilder.instance_url);
|
|
699
|
-
const sfdcUrl = new sfdcUrl_1.SfdcUrl(instanceUrl);
|
|
700
|
-
try {
|
|
701
|
-
// Check if the url is resolvable. This can fail when my-domains have not been replicated.
|
|
702
|
-
await sfdcUrl.lookup();
|
|
703
|
-
authFields.instanceUrl = instanceUrl;
|
|
704
|
-
}
|
|
705
|
-
catch (err) {
|
|
706
|
-
this.logger.debug(
|
|
707
|
-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
708
|
-
`Instance URL [${authFieldsBuilder.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
|
|
709
|
-
authFields.instanceUrl = options.loginUrl;
|
|
710
|
-
}
|
|
711
|
-
return authFields;
|
|
712
|
-
}
|
|
713
|
-
async tryJwtAuth(clientId, loginUrl, audienceUrl, privateKeyContents) {
|
|
714
|
-
const jwtToken = jwt.sign({
|
|
715
|
-
iss: clientId,
|
|
716
|
-
sub: this.getUsername(),
|
|
717
|
-
aud: audienceUrl,
|
|
718
|
-
exp: Date.now() + 300,
|
|
719
|
-
}, privateKeyContents, {
|
|
720
|
-
algorithm: 'RS256',
|
|
721
|
-
});
|
|
722
|
-
const oauth2 = new jsforce_1.JwtOAuth2({ loginUrl });
|
|
723
|
-
// jsforce has it types as any
|
|
724
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
725
|
-
return (0, ts_types_1.ensureJsonMap)(await oauth2.jwtAuthorize(jwtToken));
|
|
726
|
-
}
|
|
727
|
-
// Build OAuth config for a refresh token auth flow
|
|
728
|
-
async buildRefreshTokenConfig(options) {
|
|
729
|
-
// Ideally, this would be removed at some point in the distant future when all auth files
|
|
730
|
-
// now have the clientId stored in it.
|
|
731
|
-
if (!options.clientId) {
|
|
732
|
-
options.clientId = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
|
|
733
|
-
options.clientSecret = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientSecret;
|
|
734
|
-
}
|
|
735
|
-
if (!options.redirectUri) {
|
|
736
|
-
options.redirectUri = this.getRedirectUri();
|
|
737
|
-
}
|
|
738
|
-
const oauth2 = new jsforce_1.OAuth2(options);
|
|
739
|
-
let authFieldsBuilder;
|
|
740
|
-
try {
|
|
741
|
-
authFieldsBuilder = await oauth2.refreshToken((0, ts_types_1.ensure)(options.refreshToken));
|
|
742
|
-
}
|
|
743
|
-
catch (err) {
|
|
744
|
-
throw messages.createError('refreshTokenAuthError', [err.message]);
|
|
745
|
-
}
|
|
746
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
747
|
-
// @ts-ignore
|
|
748
|
-
const { orgId } = parseIdUrl(authFieldsBuilder.id);
|
|
749
|
-
let username = this.getUsername();
|
|
750
|
-
if (!username) {
|
|
751
|
-
const userInfo = await this.retrieveUserInfo(authFieldsBuilder.instance_url, authFieldsBuilder.access_token);
|
|
752
|
-
username = (0, ts_types_1.ensureString)(userInfo?.username);
|
|
753
|
-
}
|
|
754
|
-
return {
|
|
755
|
-
orgId,
|
|
756
|
-
username,
|
|
757
|
-
accessToken: authFieldsBuilder.access_token,
|
|
758
|
-
instanceUrl: authFieldsBuilder.instance_url,
|
|
759
|
-
loginUrl: options.loginUrl ?? authFieldsBuilder.instance_url,
|
|
760
|
-
refreshToken: options.refreshToken,
|
|
761
|
-
clientId: options.clientId,
|
|
762
|
-
clientSecret: options.clientSecret,
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge
|
|
767
|
-
*
|
|
768
|
-
* @param options The oauth options
|
|
769
|
-
* @param oauth2 The oauth2 extension that includes a code_challenge
|
|
770
|
-
*/
|
|
771
|
-
async exchangeToken(options, oauth2 = new jsforce_1.OAuth2(options)) {
|
|
772
|
-
if (!oauth2.redirectUri) {
|
|
773
|
-
oauth2.redirectUri = this.getRedirectUri();
|
|
774
|
-
}
|
|
775
|
-
if (!oauth2.clientId) {
|
|
776
|
-
oauth2.clientId = this.getClientId();
|
|
777
|
-
}
|
|
778
|
-
// Exchange the auth code for an access token and refresh token.
|
|
779
|
-
let authFields;
|
|
780
|
-
try {
|
|
781
|
-
this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`);
|
|
782
|
-
authFields = await oauth2.requestToken((0, ts_types_1.ensure)(options.authCode));
|
|
783
|
-
}
|
|
784
|
-
catch (err) {
|
|
785
|
-
throw messages.createError('authCodeExchangeError', [err.message]);
|
|
786
|
-
}
|
|
787
|
-
const { orgId } = parseIdUrl(authFields.id);
|
|
788
|
-
let username = this.getUsername();
|
|
789
|
-
// Only need to query for the username if it isn't known. For example, a new auth code exchange
|
|
790
|
-
// rather than refreshing a token on an existing connection.
|
|
791
|
-
if (!username) {
|
|
792
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
793
|
-
// @ts-ignore
|
|
794
|
-
const userInfo = await this.retrieveUserInfo(authFields.instance_url, authFields.access_token);
|
|
795
|
-
username = userInfo?.username;
|
|
796
|
-
}
|
|
797
|
-
return {
|
|
798
|
-
accessToken: authFields.access_token,
|
|
799
|
-
instanceUrl: authFields.instance_url,
|
|
800
|
-
orgId,
|
|
801
|
-
username,
|
|
802
|
-
loginUrl: options.loginUrl ?? authFields.instance_url,
|
|
803
|
-
refreshToken: authFields.refresh_token,
|
|
804
|
-
clientId: options.clientId,
|
|
805
|
-
clientSecret: options.clientSecret,
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
async retrieveUserInfo(instanceUrl, accessToken) {
|
|
809
|
-
// Make a REST call for the username directly. Normally this is done via a connection
|
|
810
|
-
// but we don't want to create circular dependencies or lots of snowflakes
|
|
811
|
-
// within this file to support it.
|
|
812
|
-
const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
|
|
813
|
-
const instance = (0, ts_types_1.ensure)(instanceUrl);
|
|
814
|
-
const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
|
|
815
|
-
const userInfoUrl = `${baseUrl.toString()}services/oauth2/userinfo`;
|
|
816
|
-
const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
|
|
817
|
-
try {
|
|
818
|
-
this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${userInfoUrl}`);
|
|
819
|
-
let response = await new transport_1.default().httpRequest({ url: userInfoUrl, method: 'GET', headers });
|
|
820
|
-
if (response.statusCode >= 400) {
|
|
821
|
-
this.throwUserGetException(response);
|
|
822
|
-
}
|
|
823
|
-
else {
|
|
824
|
-
const userInfoJson = (0, kit_1.parseJsonMap)(response.body);
|
|
825
|
-
const url = `${baseUrl.toString()}/services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`;
|
|
826
|
-
this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`);
|
|
827
|
-
response = await new transport_1.default().httpRequest({ url, method: 'GET', headers });
|
|
828
|
-
if (response.statusCode >= 400) {
|
|
829
|
-
this.throwUserGetException(response);
|
|
830
|
-
}
|
|
831
|
-
else {
|
|
832
|
-
// eslint-disable-next-line camelcase
|
|
833
|
-
userInfoJson.preferred_username = (0, kit_1.parseJsonMap)(response.body).Username;
|
|
834
|
-
}
|
|
835
|
-
return { username: userInfoJson.preferred_username, organizationId: userInfoJson.organization_id };
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
catch (err) {
|
|
839
|
-
throw messages.createError('authCodeUsernameRetrievalError', [err.message]);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* Given an error while getting the User object, handle different possibilities of response.body.
|
|
844
|
-
*
|
|
845
|
-
* @param response
|
|
846
|
-
* @private
|
|
847
|
-
*/
|
|
848
|
-
throwUserGetException(response) {
|
|
849
|
-
let errorMsg = '';
|
|
850
|
-
const bodyAsString = response.body ?? JSON.stringify({ message: 'UNKNOWN', errorCode: 'UNKNOWN' });
|
|
851
|
-
try {
|
|
852
|
-
const body = (0, kit_1.parseJson)(bodyAsString);
|
|
853
|
-
if ((0, ts_types_1.isArray)(body)) {
|
|
854
|
-
errorMsg = body.map((line) => line.message ?? line.errorCode ?? 'UNKNOWN').join(os.EOL);
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
errorMsg = body.message ?? body.errorCode ?? 'UNKNOWN';
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
catch (err) {
|
|
861
|
-
errorMsg = `${bodyAsString}`;
|
|
862
|
-
}
|
|
863
|
-
throw new sfError_1.SfError(errorMsg);
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Returns `true` if the org is a Dev Hub.
|
|
867
|
-
*
|
|
868
|
-
* Check access to the ScratchOrgInfo object to determine if the org is a dev hub.
|
|
869
|
-
*/
|
|
870
|
-
async determineIfDevHub(instanceUrl, accessToken) {
|
|
871
|
-
// Make a REST call for the ScratchOrgInfo obj directly. Normally this is done via a connection
|
|
872
|
-
// but we don't want to create circular dependencies or lots of snowflakes
|
|
873
|
-
// within this file to support it.
|
|
874
|
-
const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
|
|
875
|
-
const instance = (0, ts_types_1.ensure)(instanceUrl);
|
|
876
|
-
const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
|
|
877
|
-
const scratchOrgInfoUrl = `${baseUrl.toString()}/services/data/${apiVersion}/query?q=SELECT%20Id%20FROM%20ScratchOrgInfo%20limit%201`;
|
|
878
|
-
const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
|
|
879
|
-
try {
|
|
880
|
-
const res = await new transport_1.default().httpRequest({ url: scratchOrgInfoUrl, method: 'GET', headers });
|
|
881
|
-
if (res.statusCode >= 400) {
|
|
882
|
-
return false;
|
|
883
|
-
}
|
|
884
|
-
return true;
|
|
885
|
-
}
|
|
886
|
-
catch (err) {
|
|
887
|
-
/* Not a dev hub */
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
exports.AuthInfo = AuthInfo;
|
|
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 class-methods-use-this */
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.AuthInfo = exports.DEFAULT_CONNECTED_APP_INFO = void 0;
|
|
11
|
+
const crypto_1 = require("crypto");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const kit_1 = require("@salesforce/kit");
|
|
16
|
+
const ts_types_1 = require("@salesforce/ts-types");
|
|
17
|
+
const jsforce_1 = require("jsforce");
|
|
18
|
+
const transport_1 = require("jsforce/lib/transport");
|
|
19
|
+
const jwt = require("jsonwebtoken");
|
|
20
|
+
const config_1 = require("../config/config");
|
|
21
|
+
const configAggregator_1 = require("../config/configAggregator");
|
|
22
|
+
const logger_1 = require("../logger");
|
|
23
|
+
const sfError_1 = require("../sfError");
|
|
24
|
+
const sfdc_1 = require("../util/sfdc");
|
|
25
|
+
const stateAggregator_1 = require("../stateAggregator");
|
|
26
|
+
const messages_1 = require("../messages");
|
|
27
|
+
const sfdcUrl_1 = require("../util/sfdcUrl");
|
|
28
|
+
const connection_1 = require("./connection");
|
|
29
|
+
const orgConfigProperties_1 = require("./orgConfigProperties");
|
|
30
|
+
const org_1 = require("./org");
|
|
31
|
+
messages_1.Messages.importMessagesDirectory(__dirname);
|
|
32
|
+
const messages = messages_1.Messages.load('@salesforce/core', 'core', [
|
|
33
|
+
'authInfoCreationError',
|
|
34
|
+
'authInfoOverwriteError',
|
|
35
|
+
'namedOrgNotFound',
|
|
36
|
+
'orgDataNotAvailableError',
|
|
37
|
+
'orgDataNotAvailableError.actions',
|
|
38
|
+
'refreshTokenAuthError',
|
|
39
|
+
'jwtAuthErrors',
|
|
40
|
+
'authCodeUsernameRetrievalError',
|
|
41
|
+
'authCodeExchangeError',
|
|
42
|
+
'missingClientId',
|
|
43
|
+
]);
|
|
44
|
+
// parses the id field returned from jsForce oauth2 methods to get
|
|
45
|
+
// user ID and org ID.
|
|
46
|
+
function parseIdUrl(idUrl) {
|
|
47
|
+
const idUrls = idUrl.split('/');
|
|
48
|
+
const userId = idUrls.pop();
|
|
49
|
+
const orgId = idUrls.pop();
|
|
50
|
+
return {
|
|
51
|
+
userId,
|
|
52
|
+
orgId,
|
|
53
|
+
url: idUrl,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
exports.DEFAULT_CONNECTED_APP_INFO = {
|
|
57
|
+
clientId: 'PlatformCLI',
|
|
58
|
+
// Legacy. The connected app info is owned by the thing that
|
|
59
|
+
// creates new AuthInfos. Currently that is the auth:* commands which
|
|
60
|
+
// aren't owned by this core library. These values need to be here
|
|
61
|
+
// for any old auth files where the id and secret aren't stored.
|
|
62
|
+
//
|
|
63
|
+
// Ideally, this would be removed at some point in the distant future
|
|
64
|
+
// when all auth files now have the clientId stored in it.
|
|
65
|
+
legacyClientId: 'SalesforceDevelopmentExperience',
|
|
66
|
+
legacyClientSecret: '1384510088588713504',
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Handles persistence and fetching of user authentication information using
|
|
70
|
+
* JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
|
|
71
|
+
* use to keep tokens active. An AuthInfo can also be created with an access
|
|
72
|
+
* token, but AuthInfos created with access tokens can't be persisted to disk.
|
|
73
|
+
*
|
|
74
|
+
* **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm)
|
|
75
|
+
*
|
|
76
|
+
* **See** [Salesforce DX Usernames and Orgs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm)
|
|
77
|
+
*
|
|
78
|
+
* ```
|
|
79
|
+
* // Creating a new authentication file.
|
|
80
|
+
* const authInfo = await AuthInfo.create({
|
|
81
|
+
* username: myAdminUsername,
|
|
82
|
+
* oauth2Options: {
|
|
83
|
+
* loginUrl, authCode, clientId, clientSecret
|
|
84
|
+
* }
|
|
85
|
+
* );
|
|
86
|
+
* authInfo.save();
|
|
87
|
+
*
|
|
88
|
+
* // Creating an authorization info with an access token.
|
|
89
|
+
* const authInfo = await AuthInfo.create({
|
|
90
|
+
* username: accessToken
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // Using an existing authentication file.
|
|
94
|
+
* const authInfo = await AuthInfo.create({
|
|
95
|
+
* username: myAdminUsername
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* // Using the AuthInfo
|
|
99
|
+
* const connection = await Connection.create({ authInfo });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
class AuthInfo extends kit_1.AsyncOptionalCreatable {
|
|
103
|
+
/**
|
|
104
|
+
* Constructor
|
|
105
|
+
* **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.**
|
|
106
|
+
*
|
|
107
|
+
* @param options The options for the class instance
|
|
108
|
+
*/
|
|
109
|
+
constructor(options) {
|
|
110
|
+
super(options);
|
|
111
|
+
// Possibly overridden in create
|
|
112
|
+
this.usingAccessToken = false;
|
|
113
|
+
this.options = options ?? {};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Returns the default instance url
|
|
117
|
+
*
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
static getDefaultInstanceUrl() {
|
|
121
|
+
const configuredInstanceUrl = configAggregator_1.ConfigAggregator.getValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL).value;
|
|
122
|
+
return configuredInstanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get a list of all authorizations based on auth files stored in the global directory.
|
|
126
|
+
* One can supply a filter (see @param orgAuthFilter) and calling this function without
|
|
127
|
+
* a filter will return all authorizations.
|
|
128
|
+
*
|
|
129
|
+
* @param orgAuthFilter A predicate function that returns true for those org authorizations that are to be retained.
|
|
130
|
+
*
|
|
131
|
+
* @returns {Promise<OrgAuthorization[]>}
|
|
132
|
+
*/
|
|
133
|
+
static async listAllAuthorizations(orgAuthFilter = (orgAuth) => !!orgAuth) {
|
|
134
|
+
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
|
|
135
|
+
const config = (await configAggregator_1.ConfigAggregator.create()).getConfigInfo();
|
|
136
|
+
const orgs = await stateAggregator.orgs.readAll();
|
|
137
|
+
const final = [];
|
|
138
|
+
for (const org of orgs) {
|
|
139
|
+
const username = (0, ts_types_1.ensureString)(org.username);
|
|
140
|
+
const aliases = stateAggregator.aliases.getAll(username) ?? undefined;
|
|
141
|
+
// Get a list of configuration values that are set to either the username or one
|
|
142
|
+
// of the aliases
|
|
143
|
+
const configs = config
|
|
144
|
+
.filter((c) => aliases.includes(c.value) || c.value === username)
|
|
145
|
+
.map((c) => c.key);
|
|
146
|
+
try {
|
|
147
|
+
// prevent ConfigFile collision bug
|
|
148
|
+
// eslint-disable-next-line no-await-in-loop
|
|
149
|
+
const authInfo = await AuthInfo.create({ username });
|
|
150
|
+
const { orgId, instanceUrl, devHubUsername, expirationDate, isDevHub } = authInfo.getFields();
|
|
151
|
+
final.push({
|
|
152
|
+
aliases,
|
|
153
|
+
configs,
|
|
154
|
+
username,
|
|
155
|
+
instanceUrl,
|
|
156
|
+
isScratchOrg: Boolean(devHubUsername),
|
|
157
|
+
isDevHub: isDevHub ?? false,
|
|
158
|
+
// eslint-disable-next-line no-await-in-loop
|
|
159
|
+
isSandbox: await stateAggregator.sandboxes.hasFile(orgId),
|
|
160
|
+
orgId: orgId,
|
|
161
|
+
accessToken: authInfo.getConnectionOptions().accessToken,
|
|
162
|
+
oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
|
|
163
|
+
isExpired: Boolean(devHubUsername) && expirationDate
|
|
164
|
+
? new Date((0, ts_types_1.ensureString)(expirationDate)).getTime() < new Date().getTime()
|
|
165
|
+
: 'unknown',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
final.push({
|
|
170
|
+
aliases,
|
|
171
|
+
configs,
|
|
172
|
+
username,
|
|
173
|
+
orgId: org.orgId,
|
|
174
|
+
instanceUrl: org.instanceUrl,
|
|
175
|
+
accessToken: undefined,
|
|
176
|
+
oauthMethod: 'unknown',
|
|
177
|
+
error: err.message,
|
|
178
|
+
isExpired: 'unknown',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return final.filter(orgAuthFilter);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns true if one or more authentications are persisted.
|
|
186
|
+
*/
|
|
187
|
+
static async hasAuthentications() {
|
|
188
|
+
try {
|
|
189
|
+
const auths = await (await stateAggregator_1.StateAggregator.getInstance()).orgs.list();
|
|
190
|
+
return !(0, kit_1.isEmpty)(auths);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const error = err;
|
|
194
|
+
if (error.name === 'OrgDataNotAvailableError' || error.code === 'ENOENT') {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the authorization URL.
|
|
202
|
+
*
|
|
203
|
+
* @param options The options to generate the URL.
|
|
204
|
+
*/
|
|
205
|
+
static getAuthorizationUrl(options, oauth2) {
|
|
206
|
+
// Always use a verifier for enhanced security
|
|
207
|
+
options.useVerifier = true;
|
|
208
|
+
const oauth2Verifier = oauth2 ?? new jsforce_1.OAuth2(options);
|
|
209
|
+
// The state parameter allows the redirectUri callback listener to ignore request
|
|
210
|
+
// that don't contain the state value.
|
|
211
|
+
const params = {
|
|
212
|
+
state: (0, crypto_1.randomBytes)(Math.ceil(6)).toString('hex'),
|
|
213
|
+
prompt: 'login',
|
|
214
|
+
// Default connected app is 'refresh_token api web'
|
|
215
|
+
scope: options.scope ?? kit_1.env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'),
|
|
216
|
+
};
|
|
217
|
+
return oauth2Verifier.getAuthorizationUrl(params);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```
|
|
224
|
+
* await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl));
|
|
225
|
+
* ```
|
|
226
|
+
* @param sfdxAuthUrl
|
|
227
|
+
*/
|
|
228
|
+
static parseSfdxAuthUrl(sfdxAuthUrl) {
|
|
229
|
+
const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)/);
|
|
230
|
+
if (!match) {
|
|
231
|
+
throw new sfError_1.SfError('Invalid SFDX auth URL. Must be in the format "force://<clientId>:<clientSecret>:<refreshToken>@<instanceUrl>". Note that the SFDX auth URL uses the "force" protocol, and not "http" or "https". Also note that the "instanceUrl" inside the SFDX auth URL doesn\'t include the protocol ("https://").', 'INVALID_SFDX_AUTH_URL');
|
|
232
|
+
}
|
|
233
|
+
const [, clientId, clientSecret, refreshToken, loginUrl] = match;
|
|
234
|
+
return {
|
|
235
|
+
clientId,
|
|
236
|
+
clientSecret,
|
|
237
|
+
refreshToken,
|
|
238
|
+
loginUrl: `https://${loginUrl}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Given a set of decrypted fields and an authInfo, determine if the org belongs to an available
|
|
243
|
+
* dev hub.
|
|
244
|
+
*
|
|
245
|
+
* @param fields
|
|
246
|
+
* @param orgAuthInfo
|
|
247
|
+
*/
|
|
248
|
+
static async identifyPossibleScratchOrgs(fields, orgAuthInfo) {
|
|
249
|
+
// fields property is passed in because the consumers of this method have performed the decrypt.
|
|
250
|
+
// This is so we don't have to call authInfo.getFields(true) and decrypt again OR accidentally save an
|
|
251
|
+
// authInfo before it is necessary.
|
|
252
|
+
const logger = await logger_1.Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' });
|
|
253
|
+
// return if we already know the hub org we know it is a devhub or prod-like or no orgId present
|
|
254
|
+
if (fields.isDevHub || fields.devHubUsername || !fields.orgId)
|
|
255
|
+
return;
|
|
256
|
+
logger.debug('getting devHubs');
|
|
257
|
+
// TODO: return if url is not sandbox-like to avoid constantly asking about production orgs
|
|
258
|
+
// TODO: someday we make this easier by asking the org if it is a scratch org
|
|
259
|
+
const hubAuthInfos = await AuthInfo.getDevHubAuthInfos();
|
|
260
|
+
logger.debug(`found ${hubAuthInfos.length} DevHubs`);
|
|
261
|
+
if (hubAuthInfos.length === 0)
|
|
262
|
+
return;
|
|
263
|
+
// ask all those orgs if they know this orgId
|
|
264
|
+
await Promise.all(hubAuthInfos.map(async (hubAuthInfo) => {
|
|
265
|
+
try {
|
|
266
|
+
const data = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId);
|
|
267
|
+
if (data.totalSize > 0) {
|
|
268
|
+
// if any return a result
|
|
269
|
+
logger.debug(`found orgId ${fields.orgId} in devhub ${hubAuthInfo.username}`);
|
|
270
|
+
try {
|
|
271
|
+
await orgAuthInfo.save({ ...fields, devHubUsername: hubAuthInfo.username });
|
|
272
|
+
logger.debug(`set ${hubAuthInfo.username} as devhub for scratch org ${orgAuthInfo.getUsername()}`);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error);
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Find all dev hubs available in the local environment.
|
|
286
|
+
*/
|
|
287
|
+
static async getDevHubAuthInfos() {
|
|
288
|
+
return AuthInfo.listAllAuthorizations((possibleHub) => possibleHub?.isDevHub ?? false);
|
|
289
|
+
}
|
|
290
|
+
static async queryScratchOrg(devHubUsername, scratchOrgId) {
|
|
291
|
+
const devHubOrg = await org_1.Org.create({ aliasOrUsername: devHubUsername });
|
|
292
|
+
const conn = devHubOrg.getConnection();
|
|
293
|
+
const data = await conn.query(`select Id from ScratchOrgInfo where ScratchOrg = '${sfdc_1.sfdc.trimTo15(scratchOrgId)}'`);
|
|
294
|
+
return data;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get the username.
|
|
298
|
+
*/
|
|
299
|
+
getUsername() {
|
|
300
|
+
return this.username;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Returns true if `this` is using the JWT flow.
|
|
304
|
+
*/
|
|
305
|
+
isJwt() {
|
|
306
|
+
const { refreshToken, privateKey } = this.getFields();
|
|
307
|
+
return !refreshToken && !!privateKey;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Returns true if `this` is using an access token flow.
|
|
311
|
+
*/
|
|
312
|
+
isAccessTokenFlow() {
|
|
313
|
+
const { refreshToken, privateKey } = this.getFields();
|
|
314
|
+
return !refreshToken && !privateKey;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Returns true if `this` is using the oauth flow.
|
|
318
|
+
*/
|
|
319
|
+
isOauth() {
|
|
320
|
+
return !this.isAccessTokenFlow() && !this.isJwt();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Returns true if `this` is using the refresh token flow.
|
|
324
|
+
*/
|
|
325
|
+
isRefreshTokenFlow() {
|
|
326
|
+
const { refreshToken, authCode } = this.getFields();
|
|
327
|
+
return !authCode && !!refreshToken;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Updates the cache and persists the authentication fields (encrypted).
|
|
331
|
+
*
|
|
332
|
+
* @param authData New data to save.
|
|
333
|
+
*/
|
|
334
|
+
async save(authData) {
|
|
335
|
+
this.update(authData);
|
|
336
|
+
const username = (0, ts_types_1.ensure)(this.getUsername());
|
|
337
|
+
if (sfdc_1.sfdc.matchesAccessToken(username)) {
|
|
338
|
+
this.logger.debug('Username is an accesstoken. Skip saving authinfo to disk.');
|
|
339
|
+
return this;
|
|
340
|
+
}
|
|
341
|
+
await this.stateAggregator.orgs.write(username);
|
|
342
|
+
this.logger.info(`Saved auth info for username: ${username}`);
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Update the authorization fields, encrypting sensitive fields, but do not persist.
|
|
347
|
+
* For convenience `this` object is returned.
|
|
348
|
+
*
|
|
349
|
+
* @param authData Authorization fields to update.
|
|
350
|
+
*/
|
|
351
|
+
update(authData) {
|
|
352
|
+
if (authData && (0, ts_types_1.isPlainObject)(authData)) {
|
|
353
|
+
this.username = authData.username ?? this.username;
|
|
354
|
+
this.stateAggregator.orgs.update(this.username, authData);
|
|
355
|
+
this.logger.info(`Updated auth info for username: ${this.username}`);
|
|
356
|
+
}
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get the auth fields (decrypted) needed to make a connection.
|
|
361
|
+
*/
|
|
362
|
+
getConnectionOptions() {
|
|
363
|
+
let opts;
|
|
364
|
+
const decryptedCopy = this.getFields(true);
|
|
365
|
+
const { accessToken, instanceUrl, loginUrl } = decryptedCopy;
|
|
366
|
+
if (this.isAccessTokenFlow()) {
|
|
367
|
+
this.logger.info('Returning fields for a connection using access token.');
|
|
368
|
+
// Just auth with the accessToken
|
|
369
|
+
opts = { accessToken, instanceUrl, loginUrl };
|
|
370
|
+
}
|
|
371
|
+
else if (this.isJwt()) {
|
|
372
|
+
this.logger.info('Returning fields for a connection using JWT config.');
|
|
373
|
+
opts = {
|
|
374
|
+
accessToken,
|
|
375
|
+
instanceUrl,
|
|
376
|
+
refreshFn: this.refreshFn.bind(this),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// @TODO: figure out loginUrl and redirectUri (probably get from config class)
|
|
381
|
+
//
|
|
382
|
+
// redirectUri: org.config.getOauthCallbackUrl()
|
|
383
|
+
// loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl
|
|
384
|
+
this.logger.info('Returning fields for a connection using OAuth config.');
|
|
385
|
+
// Decrypt a user provided client secret or use the default.
|
|
386
|
+
opts = {
|
|
387
|
+
oauth2: {
|
|
388
|
+
loginUrl: instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION,
|
|
389
|
+
clientId: this.getClientId(),
|
|
390
|
+
redirectUri: this.getRedirectUri(),
|
|
391
|
+
},
|
|
392
|
+
accessToken,
|
|
393
|
+
instanceUrl,
|
|
394
|
+
refreshFn: this.refreshFn.bind(this),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// decrypt the fields
|
|
398
|
+
return opts;
|
|
399
|
+
}
|
|
400
|
+
getClientId() {
|
|
401
|
+
return this.getFields()?.clientId ?? exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
|
|
402
|
+
}
|
|
403
|
+
getRedirectUri() {
|
|
404
|
+
return 'http://localhost:1717/OauthRedirect';
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get the authorization fields.
|
|
408
|
+
*
|
|
409
|
+
* @param decrypt Decrypt the fields.
|
|
410
|
+
*/
|
|
411
|
+
getFields(decrypt) {
|
|
412
|
+
return this.stateAggregator.orgs.get(this.username, decrypt) ?? {};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get the org front door (used for web based oauth flows)
|
|
416
|
+
*/
|
|
417
|
+
getOrgFrontDoorUrl() {
|
|
418
|
+
const authFields = this.getFields(true);
|
|
419
|
+
const base = (0, ts_types_1.ensureString)(authFields.instanceUrl).replace(/\/+$/, '');
|
|
420
|
+
const accessToken = (0, ts_types_1.ensureString)(authFields.accessToken);
|
|
421
|
+
return `${base}/secur/frontdoor.jsp?sid=${accessToken}`;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Returns true if this org is using access token auth.
|
|
425
|
+
*/
|
|
426
|
+
isUsingAccessToken() {
|
|
427
|
+
return this.usingAccessToken;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get the SFDX Auth URL.
|
|
431
|
+
*
|
|
432
|
+
* **See** [SFDX Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_auth.htm#cli_reference_force_auth)
|
|
433
|
+
*/
|
|
434
|
+
getSfdxAuthUrl() {
|
|
435
|
+
const decryptedFields = this.getFields(true);
|
|
436
|
+
const instanceUrl = (0, ts_types_1.ensure)(decryptedFields.instanceUrl, 'undefined instanceUrl').replace(/^https?:\/\//, '');
|
|
437
|
+
let sfdxAuthUrl = 'force://';
|
|
438
|
+
if (decryptedFields.clientId) {
|
|
439
|
+
sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret ?? ''}:`;
|
|
440
|
+
}
|
|
441
|
+
sfdxAuthUrl += `${(0, ts_types_1.ensure)(decryptedFields.refreshToken, 'undefined refreshToken')}@${instanceUrl}`;
|
|
442
|
+
return sfdxAuthUrl;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Convenience function to handle typical side effects encountered when dealing with an AuthInfo.
|
|
446
|
+
* Given the values supplied in parameter sideEffects, this function will set auth alias, default auth
|
|
447
|
+
* and default dev hub.
|
|
448
|
+
*
|
|
449
|
+
* @param sideEffects - instance of AuthSideEffects
|
|
450
|
+
*/
|
|
451
|
+
async handleAliasAndDefaultSettings(sideEffects) {
|
|
452
|
+
if (sideEffects.alias ||
|
|
453
|
+
sideEffects.setDefault ||
|
|
454
|
+
sideEffects.setDefaultDevHub ||
|
|
455
|
+
typeof sideEffects.setTracksSource === 'boolean') {
|
|
456
|
+
if (sideEffects.alias)
|
|
457
|
+
await this.setAlias(sideEffects.alias);
|
|
458
|
+
if (sideEffects.setDefault)
|
|
459
|
+
await this.setAsDefault({ org: true });
|
|
460
|
+
if (sideEffects.setDefaultDevHub)
|
|
461
|
+
await this.setAsDefault({ devHub: true });
|
|
462
|
+
if (typeof sideEffects.setTracksSource === 'boolean') {
|
|
463
|
+
await this.save({ tracksSource: sideEffects.setTracksSource });
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
await this.save();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Set the target-env (default) or the target-dev-hub to the alias if
|
|
472
|
+
* it exists otherwise to the username. Method will try to set the local
|
|
473
|
+
* config first but will default to global config if that fails.
|
|
474
|
+
*
|
|
475
|
+
* @param options
|
|
476
|
+
*/
|
|
477
|
+
async setAsDefault(options = { org: true }) {
|
|
478
|
+
let config;
|
|
479
|
+
// if we fail to create the local config, default to the global config
|
|
480
|
+
try {
|
|
481
|
+
config = await config_1.Config.create({ isGlobal: false });
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
config = await config_1.Config.create({ isGlobal: true });
|
|
485
|
+
}
|
|
486
|
+
const username = (0, ts_types_1.ensureString)(this.getUsername());
|
|
487
|
+
const alias = this.stateAggregator.aliases.get(username);
|
|
488
|
+
const value = alias ?? username;
|
|
489
|
+
if (options.org) {
|
|
490
|
+
config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG, value);
|
|
491
|
+
}
|
|
492
|
+
if (options.devHub) {
|
|
493
|
+
config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB, value);
|
|
494
|
+
}
|
|
495
|
+
await config.write();
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Sets the provided alias to the username
|
|
499
|
+
*
|
|
500
|
+
* @param alias alias to set
|
|
501
|
+
*/
|
|
502
|
+
async setAlias(alias) {
|
|
503
|
+
this.stateAggregator.aliases.set(alias, this.getUsername());
|
|
504
|
+
await this.stateAggregator.aliases.write();
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Initializes an instance of the AuthInfo class.
|
|
508
|
+
*/
|
|
509
|
+
async init() {
|
|
510
|
+
this.stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
|
|
511
|
+
const username = this.options.username;
|
|
512
|
+
const authOptions = this.options.oauth2Options ?? this.options.accessTokenOptions;
|
|
513
|
+
// Must specify either username and/or options
|
|
514
|
+
if (!username && !authOptions) {
|
|
515
|
+
throw messages.createError('authInfoCreationError');
|
|
516
|
+
}
|
|
517
|
+
// If a username AND oauth options, ensure an authorization for the username doesn't
|
|
518
|
+
// already exist. Throw if it does so we don't overwrite the authorization.
|
|
519
|
+
if (username && authOptions) {
|
|
520
|
+
if (await this.stateAggregator.orgs.hasFile(username)) {
|
|
521
|
+
throw messages.createError('authInfoOverwriteError');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const oauthUsername = username ?? authOptions?.username;
|
|
525
|
+
if (oauthUsername) {
|
|
526
|
+
this.username = oauthUsername;
|
|
527
|
+
await this.stateAggregator.orgs.read(oauthUsername, false, false);
|
|
528
|
+
} // Else it will be set in initAuthOptions below.
|
|
529
|
+
// If the username is an access token, use that for auth and don't persist
|
|
530
|
+
if ((0, ts_types_1.isString)(oauthUsername) && sfdc_1.sfdc.matchesAccessToken(oauthUsername)) {
|
|
531
|
+
// Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
|
|
532
|
+
this.logger = await logger_1.Logger.child('AuthInfo');
|
|
533
|
+
const aggregator = await configAggregator_1.ConfigAggregator.create();
|
|
534
|
+
const instanceUrl = this.getInstanceUrl(aggregator, authOptions);
|
|
535
|
+
this.update({
|
|
536
|
+
accessToken: oauthUsername,
|
|
537
|
+
instanceUrl,
|
|
538
|
+
orgId: oauthUsername.split('!')[0],
|
|
539
|
+
loginUrl: instanceUrl,
|
|
540
|
+
});
|
|
541
|
+
this.usingAccessToken = true;
|
|
542
|
+
}
|
|
543
|
+
// If a username with NO oauth options, ensure authorization already exist.
|
|
544
|
+
else if (username && !authOptions && !(await this.stateAggregator.orgs.exists(username))) {
|
|
545
|
+
throw messages.createError('namedOrgNotFound', [username]);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
await this.initAuthOptions(authOptions);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
getInstanceUrl(aggregator, options) {
|
|
552
|
+
const instanceUrl = options?.instanceUrl ?? aggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.ORG_INSTANCE_URL);
|
|
553
|
+
return instanceUrl ?? sfdcUrl_1.SfdcUrl.PRODUCTION;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache
|
|
557
|
+
* or by reading from the persistence store. For convenience `this` object is returned.
|
|
558
|
+
*
|
|
559
|
+
* @param options Options to be used for creating an OAuth2 instance.
|
|
560
|
+
*
|
|
561
|
+
* **Throws** *{@link SfError}{ name: 'NamedOrgNotFoundError' }* Org information does not exist.
|
|
562
|
+
* @returns {Promise<AuthInfo>}
|
|
563
|
+
*/
|
|
564
|
+
async initAuthOptions(options) {
|
|
565
|
+
this.logger = await logger_1.Logger.child('AuthInfo');
|
|
566
|
+
// If options were passed, use those before checking cache and reading an auth file.
|
|
567
|
+
let authConfig;
|
|
568
|
+
if (options) {
|
|
569
|
+
options = (0, kit_1.cloneJson)(options);
|
|
570
|
+
if (this.isTokenOptions(options)) {
|
|
571
|
+
authConfig = options;
|
|
572
|
+
const userInfo = await this.retrieveUserInfo((0, ts_types_1.ensureString)(options.instanceUrl), (0, ts_types_1.ensureString)(options.accessToken));
|
|
573
|
+
this.update({ username: userInfo?.username, orgId: userInfo?.organizationId });
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
if (this.options.parentUsername) {
|
|
577
|
+
const parentFields = await this.loadDecryptedAuthFromConfig(this.options.parentUsername);
|
|
578
|
+
options.clientId = parentFields.clientId;
|
|
579
|
+
if (process.env.SFDX_CLIENT_SECRET) {
|
|
580
|
+
options.clientSecret = process.env.SFDX_CLIENT_SECRET;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
// Grab whatever flow is defined
|
|
584
|
+
Object.assign(options, {
|
|
585
|
+
clientSecret: parentFields.clientSecret,
|
|
586
|
+
privateKey: parentFields.privateKey ? (0, path_1.resolve)(parentFields.privateKey) : parentFields.privateKey,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// jwt flow
|
|
591
|
+
// Support both sfdx and jsforce private key values
|
|
592
|
+
if (!options.privateKey && options.privateKeyFile) {
|
|
593
|
+
options.privateKey = (0, path_1.resolve)(options.privateKeyFile);
|
|
594
|
+
}
|
|
595
|
+
if (options.privateKey) {
|
|
596
|
+
authConfig = await this.authJwt(options);
|
|
597
|
+
}
|
|
598
|
+
else if (!options.authCode && options.refreshToken) {
|
|
599
|
+
// refresh token flow (from sfdxUrl or OAuth refreshFn)
|
|
600
|
+
authConfig = await this.buildRefreshTokenConfig(options);
|
|
601
|
+
}
|
|
602
|
+
else if (this.options.oauth2 instanceof jsforce_1.OAuth2) {
|
|
603
|
+
// authcode exchange / web auth flow
|
|
604
|
+
authConfig = await this.exchangeToken(options, this.options.oauth2);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
authConfig = await this.exchangeToken(options);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
authConfig.isDevHub = await this.determineIfDevHub((0, ts_types_1.ensureString)(authConfig.instanceUrl), (0, ts_types_1.ensureString)(authConfig.accessToken));
|
|
611
|
+
if (authConfig.username)
|
|
612
|
+
await this.stateAggregator.orgs.read(authConfig.username, false, false);
|
|
613
|
+
// Update the auth fields WITH encryption
|
|
614
|
+
this.update(authConfig);
|
|
615
|
+
}
|
|
616
|
+
return this;
|
|
617
|
+
}
|
|
618
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
619
|
+
async loadDecryptedAuthFromConfig(username) {
|
|
620
|
+
// Fetch from the persisted auth file
|
|
621
|
+
const authInfo = this.stateAggregator.orgs.get(username, true);
|
|
622
|
+
if (!authInfo) {
|
|
623
|
+
throw messages.createError('namedOrgNotFound', [username]);
|
|
624
|
+
}
|
|
625
|
+
return authInfo;
|
|
626
|
+
}
|
|
627
|
+
isTokenOptions(options) {
|
|
628
|
+
// Although OAuth2Config does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in
|
|
629
|
+
// which WILL have an access token as well, but it should be considered an OAuth2Config at that point.
|
|
630
|
+
return ('accessToken' in options &&
|
|
631
|
+
!('refreshToken' in options) &&
|
|
632
|
+
!('privateKey' in options) &&
|
|
633
|
+
!('privateKeyFile' in options) &&
|
|
634
|
+
!('authCode' in options));
|
|
635
|
+
}
|
|
636
|
+
// A callback function for a connection to refresh an access token. This is used
|
|
637
|
+
// both for a JWT connection and an OAuth connection.
|
|
638
|
+
async refreshFn(conn, callback) {
|
|
639
|
+
this.logger.info('Access token has expired. Updating...');
|
|
640
|
+
try {
|
|
641
|
+
const fields = this.getFields(true);
|
|
642
|
+
await this.initAuthOptions(fields);
|
|
643
|
+
await this.save();
|
|
644
|
+
return await callback(null, fields.accessToken);
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
const error = err;
|
|
648
|
+
if (error?.message?.includes('Data Not Available')) {
|
|
649
|
+
// Set cause to keep original stacktrace
|
|
650
|
+
return await callback(messages.createError('orgDataNotAvailableError', [this.getUsername()], [], error));
|
|
651
|
+
}
|
|
652
|
+
return await callback(error);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async readJwtKey(keyFile) {
|
|
656
|
+
return fs.promises.readFile(keyFile, 'utf8');
|
|
657
|
+
}
|
|
658
|
+
// Build OAuth config for a JWT auth flow
|
|
659
|
+
async authJwt(options) {
|
|
660
|
+
if (!options.clientId) {
|
|
661
|
+
throw messages.createError('missingClientId');
|
|
662
|
+
}
|
|
663
|
+
const privateKeyContents = await this.readJwtKey((0, ts_types_1.ensureString)(options.privateKey));
|
|
664
|
+
const { loginUrl = sfdcUrl_1.SfdcUrl.PRODUCTION } = options;
|
|
665
|
+
const url = new sfdcUrl_1.SfdcUrl(loginUrl);
|
|
666
|
+
const createdOrgInstance = (this.getFields().createdOrgInstance ?? '').trim().toLowerCase();
|
|
667
|
+
const audienceUrl = await url.getJwtAudienceUrl(createdOrgInstance);
|
|
668
|
+
let authFieldsBuilder;
|
|
669
|
+
const authErrors = [];
|
|
670
|
+
// given that we can no longer depend on instance names or URls to determine audience, let's try them all
|
|
671
|
+
const loginAndAudienceUrls = (0, sfdcUrl_1.getLoginAudienceCombos)(audienceUrl, loginUrl);
|
|
672
|
+
for (const [login, audience] of loginAndAudienceUrls) {
|
|
673
|
+
try {
|
|
674
|
+
// sequentially, in probabilistic order
|
|
675
|
+
// eslint-disable-next-line no-await-in-loop
|
|
676
|
+
authFieldsBuilder = await this.tryJwtAuth(options.clientId, login, audience, privateKeyContents);
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
const error = err;
|
|
681
|
+
const message = error.message.includes('audience')
|
|
682
|
+
? `${error.message} [audience=${audience} login=${login}]`
|
|
683
|
+
: error.message;
|
|
684
|
+
authErrors.push(message);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!authFieldsBuilder) {
|
|
688
|
+
// messages.createError expects names to end in `error` and this one says Errors so do it manually.
|
|
689
|
+
throw new sfError_1.SfError(messages.getMessage('jwtAuthErrors', [authErrors.join('\n')]), 'JwtAuthError');
|
|
690
|
+
}
|
|
691
|
+
const authFields = {
|
|
692
|
+
accessToken: (0, ts_types_1.asString)(authFieldsBuilder.access_token),
|
|
693
|
+
orgId: parseIdUrl((0, ts_types_1.ensureString)(authFieldsBuilder.id)).orgId,
|
|
694
|
+
loginUrl: options.loginUrl,
|
|
695
|
+
privateKey: options.privateKey,
|
|
696
|
+
clientId: options.clientId,
|
|
697
|
+
};
|
|
698
|
+
const instanceUrl = (0, ts_types_1.ensureString)(authFieldsBuilder.instance_url);
|
|
699
|
+
const sfdcUrl = new sfdcUrl_1.SfdcUrl(instanceUrl);
|
|
700
|
+
try {
|
|
701
|
+
// Check if the url is resolvable. This can fail when my-domains have not been replicated.
|
|
702
|
+
await sfdcUrl.lookup();
|
|
703
|
+
authFields.instanceUrl = instanceUrl;
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
this.logger.debug(
|
|
707
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
708
|
+
`Instance URL [${authFieldsBuilder.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
|
|
709
|
+
authFields.instanceUrl = options.loginUrl;
|
|
710
|
+
}
|
|
711
|
+
return authFields;
|
|
712
|
+
}
|
|
713
|
+
async tryJwtAuth(clientId, loginUrl, audienceUrl, privateKeyContents) {
|
|
714
|
+
const jwtToken = jwt.sign({
|
|
715
|
+
iss: clientId,
|
|
716
|
+
sub: this.getUsername(),
|
|
717
|
+
aud: audienceUrl,
|
|
718
|
+
exp: Date.now() + 300,
|
|
719
|
+
}, privateKeyContents, {
|
|
720
|
+
algorithm: 'RS256',
|
|
721
|
+
});
|
|
722
|
+
const oauth2 = new jsforce_1.JwtOAuth2({ loginUrl });
|
|
723
|
+
// jsforce has it types as any
|
|
724
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
725
|
+
return (0, ts_types_1.ensureJsonMap)(await oauth2.jwtAuthorize(jwtToken));
|
|
726
|
+
}
|
|
727
|
+
// Build OAuth config for a refresh token auth flow
|
|
728
|
+
async buildRefreshTokenConfig(options) {
|
|
729
|
+
// Ideally, this would be removed at some point in the distant future when all auth files
|
|
730
|
+
// now have the clientId stored in it.
|
|
731
|
+
if (!options.clientId) {
|
|
732
|
+
options.clientId = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId;
|
|
733
|
+
options.clientSecret = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientSecret;
|
|
734
|
+
}
|
|
735
|
+
if (!options.redirectUri) {
|
|
736
|
+
options.redirectUri = this.getRedirectUri();
|
|
737
|
+
}
|
|
738
|
+
const oauth2 = new jsforce_1.OAuth2(options);
|
|
739
|
+
let authFieldsBuilder;
|
|
740
|
+
try {
|
|
741
|
+
authFieldsBuilder = await oauth2.refreshToken((0, ts_types_1.ensure)(options.refreshToken));
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
throw messages.createError('refreshTokenAuthError', [err.message]);
|
|
745
|
+
}
|
|
746
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
747
|
+
// @ts-ignore
|
|
748
|
+
const { orgId } = parseIdUrl(authFieldsBuilder.id);
|
|
749
|
+
let username = this.getUsername();
|
|
750
|
+
if (!username) {
|
|
751
|
+
const userInfo = await this.retrieveUserInfo(authFieldsBuilder.instance_url, authFieldsBuilder.access_token);
|
|
752
|
+
username = (0, ts_types_1.ensureString)(userInfo?.username);
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
orgId,
|
|
756
|
+
username,
|
|
757
|
+
accessToken: authFieldsBuilder.access_token,
|
|
758
|
+
instanceUrl: authFieldsBuilder.instance_url,
|
|
759
|
+
loginUrl: options.loginUrl ?? authFieldsBuilder.instance_url,
|
|
760
|
+
refreshToken: options.refreshToken,
|
|
761
|
+
clientId: options.clientId,
|
|
762
|
+
clientSecret: options.clientSecret,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge
|
|
767
|
+
*
|
|
768
|
+
* @param options The oauth options
|
|
769
|
+
* @param oauth2 The oauth2 extension that includes a code_challenge
|
|
770
|
+
*/
|
|
771
|
+
async exchangeToken(options, oauth2 = new jsforce_1.OAuth2(options)) {
|
|
772
|
+
if (!oauth2.redirectUri) {
|
|
773
|
+
oauth2.redirectUri = this.getRedirectUri();
|
|
774
|
+
}
|
|
775
|
+
if (!oauth2.clientId) {
|
|
776
|
+
oauth2.clientId = this.getClientId();
|
|
777
|
+
}
|
|
778
|
+
// Exchange the auth code for an access token and refresh token.
|
|
779
|
+
let authFields;
|
|
780
|
+
try {
|
|
781
|
+
this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`);
|
|
782
|
+
authFields = await oauth2.requestToken((0, ts_types_1.ensure)(options.authCode));
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
throw messages.createError('authCodeExchangeError', [err.message]);
|
|
786
|
+
}
|
|
787
|
+
const { orgId } = parseIdUrl(authFields.id);
|
|
788
|
+
let username = this.getUsername();
|
|
789
|
+
// Only need to query for the username if it isn't known. For example, a new auth code exchange
|
|
790
|
+
// rather than refreshing a token on an existing connection.
|
|
791
|
+
if (!username) {
|
|
792
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
793
|
+
// @ts-ignore
|
|
794
|
+
const userInfo = await this.retrieveUserInfo(authFields.instance_url, authFields.access_token);
|
|
795
|
+
username = userInfo?.username;
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
accessToken: authFields.access_token,
|
|
799
|
+
instanceUrl: authFields.instance_url,
|
|
800
|
+
orgId,
|
|
801
|
+
username,
|
|
802
|
+
loginUrl: options.loginUrl ?? authFields.instance_url,
|
|
803
|
+
refreshToken: authFields.refresh_token,
|
|
804
|
+
clientId: options.clientId,
|
|
805
|
+
clientSecret: options.clientSecret,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async retrieveUserInfo(instanceUrl, accessToken) {
|
|
809
|
+
// Make a REST call for the username directly. Normally this is done via a connection
|
|
810
|
+
// but we don't want to create circular dependencies or lots of snowflakes
|
|
811
|
+
// within this file to support it.
|
|
812
|
+
const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
|
|
813
|
+
const instance = (0, ts_types_1.ensure)(instanceUrl);
|
|
814
|
+
const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
|
|
815
|
+
const userInfoUrl = `${baseUrl.toString()}services/oauth2/userinfo`;
|
|
816
|
+
const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
|
|
817
|
+
try {
|
|
818
|
+
this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${userInfoUrl}`);
|
|
819
|
+
let response = await new transport_1.default().httpRequest({ url: userInfoUrl, method: 'GET', headers });
|
|
820
|
+
if (response.statusCode >= 400) {
|
|
821
|
+
this.throwUserGetException(response);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const userInfoJson = (0, kit_1.parseJsonMap)(response.body);
|
|
825
|
+
const url = `${baseUrl.toString()}/services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`;
|
|
826
|
+
this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`);
|
|
827
|
+
response = await new transport_1.default().httpRequest({ url, method: 'GET', headers });
|
|
828
|
+
if (response.statusCode >= 400) {
|
|
829
|
+
this.throwUserGetException(response);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// eslint-disable-next-line camelcase
|
|
833
|
+
userInfoJson.preferred_username = (0, kit_1.parseJsonMap)(response.body).Username;
|
|
834
|
+
}
|
|
835
|
+
return { username: userInfoJson.preferred_username, organizationId: userInfoJson.organization_id };
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
throw messages.createError('authCodeUsernameRetrievalError', [err.message]);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Given an error while getting the User object, handle different possibilities of response.body.
|
|
844
|
+
*
|
|
845
|
+
* @param response
|
|
846
|
+
* @private
|
|
847
|
+
*/
|
|
848
|
+
throwUserGetException(response) {
|
|
849
|
+
let errorMsg = '';
|
|
850
|
+
const bodyAsString = response.body ?? JSON.stringify({ message: 'UNKNOWN', errorCode: 'UNKNOWN' });
|
|
851
|
+
try {
|
|
852
|
+
const body = (0, kit_1.parseJson)(bodyAsString);
|
|
853
|
+
if ((0, ts_types_1.isArray)(body)) {
|
|
854
|
+
errorMsg = body.map((line) => line.message ?? line.errorCode ?? 'UNKNOWN').join(os.EOL);
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
errorMsg = body.message ?? body.errorCode ?? 'UNKNOWN';
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
catch (err) {
|
|
861
|
+
errorMsg = `${bodyAsString}`;
|
|
862
|
+
}
|
|
863
|
+
throw new sfError_1.SfError(errorMsg);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Returns `true` if the org is a Dev Hub.
|
|
867
|
+
*
|
|
868
|
+
* Check access to the ScratchOrgInfo object to determine if the org is a dev hub.
|
|
869
|
+
*/
|
|
870
|
+
async determineIfDevHub(instanceUrl, accessToken) {
|
|
871
|
+
// Make a REST call for the ScratchOrgInfo obj directly. Normally this is done via a connection
|
|
872
|
+
// but we don't want to create circular dependencies or lots of snowflakes
|
|
873
|
+
// within this file to support it.
|
|
874
|
+
const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
|
|
875
|
+
const instance = (0, ts_types_1.ensure)(instanceUrl);
|
|
876
|
+
const baseUrl = new sfdcUrl_1.SfdcUrl(instance);
|
|
877
|
+
const scratchOrgInfoUrl = `${baseUrl.toString()}/services/data/${apiVersion}/query?q=SELECT%20Id%20FROM%20ScratchOrgInfo%20limit%201`;
|
|
878
|
+
const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS);
|
|
879
|
+
try {
|
|
880
|
+
const res = await new transport_1.default().httpRequest({ url: scratchOrgInfoUrl, method: 'GET', headers });
|
|
881
|
+
if (res.statusCode >= 400) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
/* Not a dev hub */
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
exports.AuthInfo = AuthInfo;
|
|
893
893
|
//# sourceMappingURL=authInfo.js.map
|