UNPKG

31.7 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2018, salesforce.com, inc.
4 * All rights reserved.
5 * SPDX-License-Identifier: BSD-3-Clause
6 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9const kit_1 = require("@salesforce/kit");
10const ts_types_1 = require("@salesforce/ts-types");
11const crypto_1 = require("crypto");
12const dns = require("dns");
13const jsforce_1 = require("jsforce");
14// @ts-ignore No typings directly available for jsforce/lib/transport
15const Transport = require("jsforce/lib/transport");
16const jwt = require("jsonwebtoken");
17const path_1 = require("path");
18const url_1 = require("url");
19const authInfoConfig_1 = require("./config/authInfoConfig");
20const configAggregator_1 = require("./config/configAggregator");
21const connection_1 = require("./connection");
22const crypto_2 = require("./crypto");
23const global_1 = require("./global");
24const logger_1 = require("./logger");
25const sfdxError_1 = require("./sfdxError");
26const fs_1 = require("./util/fs");
27// Extend OAuth2 to add JWT Bearer Token Flow support.
28class JwtOAuth2 extends jsforce_1.OAuth2 {
29 constructor(options) {
30 super(options);
31 }
32 async jwtAuthorize(innerToken, callback) {
33 // tslint:disable-line:no-any
34 // @ts-ignore TODO: need better typings for jsforce
35 return super._postParams({
36 grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
37 assertion: innerToken
38 }, callback);
39 }
40}
41/**
42 * Extend OAuth2 to add code verifier support for the auth code (web auth) flow
43 * const oauth2 = new OAuth2WithVerifier({ loginUrl, clientSecret, clientId, redirectUri });
44 *
45 * const authUrl = oauth2.getAuthorizationUrl({
46 * state: 'foo',
47 * prompt: 'login',
48 * scope: 'api web'
49 * });
50 * console.log(authUrl);
51 * const authCode = await retrieveCode();
52 * const authInfo = await AuthInfo.create({ oauth2Options: { clientId, clientSecret, loginUrl, authCode }, oauth2});
53 * console.log(`access token: ${authInfo.getFields().accessToken}`);
54 */
55class OAuth2WithVerifier extends jsforce_1.OAuth2 {
56 constructor(options) {
57 super(options);
58 // Set a code verifier string for OAuth authorization
59 this.codeVerifier = base64UrlEscape(crypto_1.randomBytes(Math.ceil(128)).toString('base64'));
60 }
61 /**
62 * Overrides jsforce.OAuth2.getAuthorizationUrl. Get Salesforce OAuth2 authorization page
63 * URL to redirect user agent, adding a verification code for added security.
64 *
65 * @param params
66 */
67 getAuthorizationUrl(params) {
68 // code verifier must be a base 64 url encoded hash of 128 bytes of random data. Our random data is also
69 // base 64 url encoded. See Connection.create();
70 const codeChallenge = base64UrlEscape(crypto_1.createHash('sha256')
71 .update(this.codeVerifier)
72 .digest('base64'));
73 kit_1.set(params, 'code_challenge', codeChallenge);
74 return super.getAuthorizationUrl(params);
75 }
76 async requestToken(code, callback) {
77 return super.requestToken(code, callback);
78 }
79 /**
80 * Overrides jsforce.OAuth2._postParams because jsforce's oauth impl doesn't support
81 * coder_verifier and code_challenge. This enables the server to disallow trading a one-time auth code
82 * for an access/refresh token when the verifier and challenge are out of alignment.
83 *
84 * See https://github.com/jsforce/jsforce/issues/665
85 */
86 // tslint:disable-next-line:no-unused-variable
87 async _postParams(params, callback) {
88 kit_1.set(params, 'code_verifier', this.codeVerifier);
89 // @ts-ignore TODO: need better typings for jsforce
90 return super._postParams(params, callback);
91 }
92}
93exports.OAuth2WithVerifier = OAuth2WithVerifier;
94/**
95 * Salesforce URLs.
96 */
97var SfdcUrl;
98(function (SfdcUrl) {
99 SfdcUrl["SANDBOX"] = "https://test.salesforce.com";
100 SfdcUrl["PRODUCTION"] = "https://login.salesforce.com";
101})(SfdcUrl = exports.SfdcUrl || (exports.SfdcUrl = {}));
102const INTERNAL_URL_PARTS = [
103 '.internal.',
104 '.vpod.',
105 'stm.salesforce.com',
106 '.blitz.salesforce.com',
107 'mobile1.t.salesforce.com'
108];
109function isInternalUrl(loginUrl = '') {
110 return loginUrl.startsWith('https://gs1.') || INTERNAL_URL_PARTS.some(part => loginUrl.includes(part));
111}
112function getJwtAudienceUrl(options) {
113 // default audience must be...
114 let audienceUrl = SfdcUrl.PRODUCTION;
115 const loginUrl = ts_types_1.getString(options, 'loginUrl', '');
116 const createdOrgInstance = ts_types_1.getString(options, 'createdOrgInstance', '')
117 .trim()
118 .toLowerCase();
119 if (process.env.SFDX_AUDIENCE_URL) {
120 audienceUrl = process.env.SFDX_AUDIENCE_URL;
121 }
122 else if (isInternalUrl(loginUrl)) {
123 // This is for internal developers when just doing authorize;
124 audienceUrl = loginUrl;
125 }
126 else if (createdOrgInstance.startsWith('cs') || url_1.parse(loginUrl).hostname === 'test.salesforce.com') {
127 audienceUrl = SfdcUrl.SANDBOX;
128 }
129 else if (createdOrgInstance.startsWith('gs1')) {
130 audienceUrl = 'https://gs1.salesforce.com';
131 }
132 return audienceUrl;
133}
134// parses the id field returned from jsForce oauth2 methods to get
135// user ID and org ID.
136function _parseIdUrl(idUrl) {
137 const idUrls = idUrl.split('/');
138 const userId = idUrls.pop();
139 const orgId = idUrls.pop();
140 return {
141 userId,
142 orgId,
143 url: idUrl
144 };
145}
146// Legacy. The connected app info is owned by the thing that
147// creates new AuthInfos. Currently that is the auth:* commands which
148// aren't owned by this core library. These values need to be here
149// for any old auth files where the id and secret aren't stored.
150//
151// Ideally, this would be removed at some point in the distant future
152// when all auth files now have the clientId stored in it.
153const DEFAULT_CONNECTED_APP_INFO = {
154 legacyClientId: 'SalesforceDevelopmentExperience',
155 legacyClientSecret: '1384510088588713504'
156};
157class AuthInfoCrypto extends crypto_2.Crypto {
158 decryptFields(fields) {
159 return this._crypt(fields, 'decrypt');
160 }
161 encryptFields(fields) {
162 return this._crypt(fields, 'encrypt');
163 }
164 _crypt(fields, method) {
165 const copy = {};
166 for (const key of ts_types_1.keysOf(fields)) {
167 const rawValue = fields[key];
168 if (rawValue !== undefined) {
169 if (ts_types_1.isString(rawValue) && AuthInfoCrypto.encryptedFields.includes(key)) {
170 copy[key] = this[method](ts_types_1.asString(rawValue));
171 }
172 else {
173 copy[key] = rawValue;
174 }
175 }
176 }
177 return copy;
178 }
179}
180AuthInfoCrypto.encryptedFields = [
181 'accessToken',
182 'refreshToken',
183 'password',
184 'clientSecret'
185];
186// Makes a nodejs base64 encoded string compatible with rfc4648 alternative encoding for urls.
187// @param base64Encoded a nodejs base64 encoded string
188function base64UrlEscape(base64Encoded) {
189 // builtin node js base 64 encoding is not 64 url compatible.
190 // See https://toolsn.ietf.org/html/rfc4648#section-5
191 return base64Encoded
192 .replace(/\+/g, '-')
193 .replace(/\//g, '_')
194 .replace(/=/g, '');
195}
196/**
197 * Handles persistence and fetching of user authentication information using
198 * JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
199 * use to keep tokens active. An AuthInfo can also be created with an access
200 * token, but AuthInfos created with access tokens can't be persisted to disk.
201 *
202 * **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm)
203 *
204 * **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)
205 *
206 * ```
207 * // Creating a new authentication file.
208 * const authInfo = await AuthInfo.create({
209 * username: myAdminUsername,
210 * oauth2Options: {
211 * loginUrl, authCode, clientId, clientSecret
212 * }
213 * );
214 * authInfo.save();
215 *
216 * // Creating an authorization info with an access token.
217 * const authInfo = await AuthInfo.create({
218 * username: accessToken
219 * });
220 *
221 * // Using an existing authentication file.
222 * const authInfo = await AuthInfo.create({
223 * username: myAdminUsername
224 * });
225 *
226 * // Using the AuthInfo
227 * const connection = await Connection.create({ authInfo });
228 * ```
229 */
230class AuthInfo extends kit_1.AsyncCreatable {
231 /**
232 * Constructor
233 * **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.**
234 * @param options The options for the class instance
235 */
236 constructor(options) {
237 super(options);
238 // All sensitive fields are encrypted
239 this.fields = {};
240 // Possibly overridden in create
241 this.usingAccessToken = false;
242 this.options = options;
243 }
244 /**
245 * Get a list of all auth files stored in the global directory.
246 * @returns {Promise<string[]>}
247 */
248 static async listAllAuthFiles() {
249 const globalFiles = await fs_1.fs.readdir(global_1.Global.DIR);
250 const authFiles = globalFiles.filter(file => file.match(AuthInfo.authFilenameFilterRegEx));
251 // Want to throw a clean error if no files are found.
252 if (kit_1.isEmpty(authFiles)) {
253 const errConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'NoAuthInfoFound');
254 throw sfdxError_1.SfdxError.create(errConfig);
255 }
256 // At least one auth file is in the global dir.
257 return authFiles;
258 }
259 /**
260 * Returns true if one or more authentications are persisted.
261 */
262 static async hasAuthentications() {
263 try {
264 const authFiles = await this.listAllAuthFiles();
265 return !kit_1.isEmpty(authFiles);
266 }
267 catch (err) {
268 if (err.name === 'OrgDataNotAvailableError' || err.code === 'ENOENT') {
269 return false;
270 }
271 throw err;
272 }
273 }
274 /**
275 * Get the authorization URL.
276 * @param options The options to generate the URL.
277 */
278 static getAuthorizationUrl(options) {
279 const oauth2 = new OAuth2WithVerifier(options);
280 // The state parameter allows the redirectUri callback listener to ignore request
281 // that don't contain the state value.
282 const params = {
283 state: crypto_1.randomBytes(Math.ceil(6)).toString('hex'),
284 prompt: 'login',
285 scope: 'refresh_token api web'
286 };
287 return oauth2.getAuthorizationUrl(params);
288 }
289 /**
290 * Forces the auth file to be re-read from disk for a given user. Returns `true` if a value was removed.
291 * @param username The username for the auth info to re-read.
292 */
293 static clearCache(username) {
294 if (username) {
295 return AuthInfo.cache.delete(username);
296 }
297 return false;
298 }
299 /**
300 * Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`.
301 *
302 * @example
303 * ```
304 * await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl));
305 * ```
306 * @param sfdxAuthUrl
307 */
308 static parseSfdxAuthUrl(sfdxAuthUrl) {
309 const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+)@([a-zA-Z0-9._-]+)/);
310 if (!match) {
311 throw new sfdxError_1.SfdxError('Invalid sfdx auth url. Must be in the format `force://<clientId>:<clientSecret>:<refreshToken>@<loginUrl>`. The instanceUrl must not have the protocol set.', 'INVALID_SFDX_AUTH_URL');
312 }
313 const [, clientId, clientSecret, refreshToken, loginUrl] = match;
314 return {
315 clientId,
316 clientSecret,
317 refreshToken,
318 loginUrl: `https://${loginUrl}`
319 };
320 }
321 /**
322 * Get the username.
323 */
324 getUsername() {
325 return this.fields.username;
326 }
327 /**
328 * Returns true if `this` is using the JWT flow.
329 */
330 isJwt() {
331 const { refreshToken, privateKey } = this.fields;
332 return !refreshToken && !!privateKey;
333 }
334 /**
335 * Returns true if `this` is using an access token flow.
336 */
337 isAccessTokenFlow() {
338 const { refreshToken, privateKey } = this.fields;
339 return !refreshToken && !privateKey;
340 }
341 /**
342 * Returns true if `this` is using the oauth flow.
343 */
344 isOauth() {
345 return !this.isAccessTokenFlow() && !this.isJwt();
346 }
347 /**
348 * Returns true if `this` is using the refresh token flow.
349 */
350 isRefreshTokenFlow() {
351 const { refreshToken, authCode } = this.fields;
352 return !authCode && !!refreshToken;
353 }
354 /**
355 * Updates the cache and persists the authentication fields (encrypted).
356 * @param authData New data to save.
357 */
358 async save(authData) {
359 this.update(authData);
360 const username = ts_types_1.ensure(this.getUsername());
361 AuthInfo.cache.set(username, this.fields);
362 const dataToSave = kit_1.cloneJson(this.fields);
363 this.logger.debug(dataToSave);
364 const config = await authInfoConfig_1.AuthInfoConfig.create(Object.assign({}, authInfoConfig_1.AuthInfoConfig.getOptions(username), { throwOnNotFound: false }));
365 config.setContentsFromObject(dataToSave);
366 await config.write();
367 this.logger.info(`Saved auth info for username: ${this.getUsername()}`);
368 return this;
369 }
370 /**
371 * Update the authorization fields, encrypting sensitive fields, but do not persist.
372 * For convenience `this` object is returned.
373 *
374 * @param authData Authorization fields to update.
375 * @param encrypt Encrypt the fields.
376 */
377 update(authData, encrypt = true) {
378 if (authData && ts_types_1.isPlainObject(authData)) {
379 let copy = kit_1.cloneJson(authData);
380 if (encrypt) {
381 copy = this.authInfoCrypto.encryptFields(copy);
382 }
383 Object.assign(this.fields, copy);
384 this.logger.info(`Updated auth info for username: ${this.getUsername()}`);
385 }
386 return this;
387 }
388 /**
389 * Get the auth fields (decrypted) needed to make a connection.
390 */
391 getConnectionOptions() {
392 let opts;
393 const { accessToken, instanceUrl } = this.fields;
394 if (this.isAccessTokenFlow()) {
395 this.logger.info('Returning fields for a connection using access token.');
396 // Just auth with the accessToken
397 opts = { accessToken, instanceUrl };
398 }
399 else if (this.isJwt()) {
400 this.logger.info('Returning fields for a connection using JWT config.');
401 opts = {
402 accessToken,
403 instanceUrl,
404 refreshFn: this.refreshFn.bind(this)
405 };
406 }
407 else {
408 // @TODO: figure out loginUrl and redirectUri (probably get from config class)
409 //
410 // redirectUri: org.config.getOauthCallbackUrl()
411 // loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl
412 this.logger.info('Returning fields for a connection using OAuth config.');
413 // Decrypt a user provided client secret or use the default.
414 opts = {
415 oauth2: {
416 loginUrl: instanceUrl || 'https://login.salesforce.com',
417 clientId: this.fields.clientId || DEFAULT_CONNECTED_APP_INFO.legacyClientId,
418 redirectUri: 'http://localhost:1717/OauthRedirect'
419 },
420 accessToken,
421 instanceUrl,
422 refreshFn: this.refreshFn.bind(this)
423 };
424 }
425 // decrypt the fields
426 return this.authInfoCrypto.decryptFields(opts);
427 }
428 /**
429 * Get the authorization fields.
430 */
431 getFields() {
432 return this.fields;
433 }
434 /**
435 * Returns true if this org is using access token auth.
436 */
437 isUsingAccessToken() {
438 return this.usingAccessToken;
439 }
440 /**
441 * Get the SFDX Auth URL.
442 *
443 * **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)
444 */
445 getSfdxAuthUrl() {
446 const decryptedFields = this.authInfoCrypto.decryptFields(this.fields);
447 const instanceUrl = ts_types_1.ensure(decryptedFields.instanceUrl).replace(/^https?:\/\//, '');
448 let sfdxAuthUrl = 'force://';
449 if (decryptedFields.clientId) {
450 sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret || ''}:`;
451 }
452 sfdxAuthUrl += `${decryptedFields.refreshToken}@${instanceUrl}`;
453 return sfdxAuthUrl;
454 }
455 /**
456 * Initializes an instance of the AuthInfo class.
457 */
458 async init() {
459 // Must specify either username and/or options
460 const options = this.options.oauth2Options || this.options.accessTokenOptions;
461 if (!this.options.username && !(this.options.oauth2Options || this.options.accessTokenOptions)) {
462 throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthInfoCreationError');
463 }
464 // If a username AND oauth options were passed, ensure an auth file for the username doesn't
465 // already exist. Throw if it does so we don't overwrite the auth file.
466 if (this.options.username && this.options.oauth2Options) {
467 const authInfoConfig = await authInfoConfig_1.AuthInfoConfig.create(Object.assign({}, authInfoConfig_1.AuthInfoConfig.getOptions(this.options.username), { throwOnNotFound: false }));
468 if (await authInfoConfig.exists()) {
469 throw sfdxError_1.SfdxError.create(new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'AuthInfoOverwriteError', undefined, 'AuthInfoOverwriteErrorAction'));
470 }
471 }
472 this.fields.username = this.options.username || ts_types_1.getString(options, 'username') || undefined;
473 // If the username is an access token, use that for auth and don't persist
474 const accessTokenMatch = ts_types_1.isString(this.fields.username) && this.fields.username.match(/^(00D\w{12,15})![\.\w]*$/);
475 if (accessTokenMatch) {
476 // Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
477 this.logger = await logger_1.Logger.child('AuthInfo');
478 this.authInfoCrypto = await AuthInfoCrypto.create({
479 noResetOnClose: true
480 });
481 const aggregator = await configAggregator_1.ConfigAggregator.create();
482 const instanceUrl = aggregator.getPropertyValue('instanceUrl') || SfdcUrl.PRODUCTION;
483 this.update({
484 accessToken: this.options.username,
485 instanceUrl,
486 orgId: accessTokenMatch[1]
487 });
488 this.usingAccessToken = true;
489 }
490 else {
491 await this.initAuthOptions(options);
492 }
493 }
494 /**
495 * Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache
496 * or by reading from the persistence store. For convenience `this` object is returned.
497 * @param options Options to be used for creating an OAuth2 instance.
498 *
499 * **Throws** *{@link SfdxError}{ name: 'NamedOrgNotFound' }* Org information does not exist.
500 * @returns {Promise<AuthInfo>}
501 */
502 async initAuthOptions(options) {
503 this.logger = await logger_1.Logger.child('AuthInfo');
504 this.authInfoCrypto = await AuthInfoCrypto.create();
505 // If options were passed, use those before checking cache and reading an auth file.
506 let authConfig;
507 if (options) {
508 options = kit_1.cloneJson(options);
509 if (this.isTokenOptions(options)) {
510 authConfig = options;
511 }
512 else {
513 if (this.options.parentUsername) {
514 const parentUserFields = await this.loadAuthFromConfig(this.options.parentUsername);
515 const parentFields = this.authInfoCrypto.decryptFields(parentUserFields);
516 options.clientId = parentFields.clientId;
517 if (process.env.SFDX_CLIENT_SECRET) {
518 options.clientSecret = process.env.SFDX_CLIENT_SECRET;
519 }
520 else {
521 // Grab whatever flow is defined
522 Object.assign(options, {
523 clientSecret: parentFields.clientSecret,
524 privateKey: parentFields.privateKey ? path_1.resolve(parentFields.privateKey) : parentFields.privateKey
525 });
526 }
527 }
528 // jwt flow
529 // Support both sfdx and jsforce private key values
530 if (!options.privateKey && options.privateKeyFile) {
531 options.privateKey = path_1.resolve(options.privateKeyFile);
532 }
533 if (options.privateKey) {
534 authConfig = await this.buildJwtConfig(options);
535 }
536 else if (!options.authCode && options.refreshToken) {
537 // refresh token flow (from sfdxUrl or OAuth refreshFn)
538 authConfig = await this.buildRefreshTokenConfig(options);
539 }
540 else {
541 if (this.options.oauth2 instanceof OAuth2WithVerifier) {
542 // authcode exchange / web auth flow
543 authConfig = await this.exchangeToken(options, this.options.oauth2);
544 }
545 else {
546 authConfig = await this.exchangeToken(options);
547 }
548 }
549 }
550 // Update the auth fields WITH encryption
551 this.update(authConfig);
552 }
553 else {
554 authConfig = await this.loadAuthFromConfig(ts_types_1.ensure(this.getUsername()));
555 // Update the auth fields WITHOUT encryption (already encrypted)
556 this.update(authConfig, false);
557 }
558 const username = this.getUsername();
559 if (username) {
560 // Cache the fields by username (fields are encrypted)
561 AuthInfo.cache.set(username, this.fields);
562 }
563 return this;
564 }
565 async loadAuthFromConfig(username) {
566 if (AuthInfo.cache.has(username)) {
567 return ts_types_1.ensure(AuthInfo.cache.get(username));
568 }
569 else {
570 // Fetch from the persisted auth file
571 try {
572 const config = await authInfoConfig_1.AuthInfoConfig.create(Object.assign({}, authInfoConfig_1.AuthInfoConfig.getOptions(username), { throwOnNotFound: true }));
573 return config.toObject();
574 }
575 catch (e) {
576 if (e.code === 'ENOENT') {
577 throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'NamedOrgNotFound', [username]);
578 }
579 else {
580 throw e;
581 }
582 }
583 }
584 }
585 isTokenOptions(options) {
586 // Although OAuth2Options does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in
587 // which WILL have an access token as well, but it should be considered an OAuth2Options at that point.
588 return ('accessToken' in options &&
589 !('refreshToken' in options) &&
590 !('privateKey' in options) &&
591 !('privateKeyFile' in options) &&
592 !('authCode' in options));
593 }
594 // A callback function for a connection to refresh an access token. This is used
595 // both for a JWT connection and an OAuth connection.
596 async refreshFn(conn, callback) {
597 this.logger.info('Access token has expired. Updating...');
598 try {
599 const fields = this.authInfoCrypto.decryptFields(this.fields);
600 await this.initAuthOptions(fields);
601 await this.save();
602 return await callback(null, fields.accessToken);
603 }
604 catch (err) {
605 if (err.message && err.message.includes('Data Not Available')) {
606 const errConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'OrgDataNotAvailableError', [
607 this.getUsername()
608 ]);
609 for (let i = 1; i < 5; i++) {
610 errConfig.addAction(`OrgDataNotAvailableErrorAction${i}`);
611 }
612 return await callback(sfdxError_1.SfdxError.create(errConfig));
613 }
614 return await callback(err);
615 }
616 }
617 // Build OAuth config for a JWT auth flow
618 async buildJwtConfig(options) {
619 const privateKeyContents = await fs_1.fs.readFile(ts_types_1.ensure(options.privateKey), 'utf8');
620 const audienceUrl = getJwtAudienceUrl(options);
621 const jwtToken = await jwt.sign({
622 iss: options.clientId,
623 sub: this.getUsername(),
624 aud: audienceUrl,
625 exp: Date.now() + 300
626 }, privateKeyContents, {
627 algorithm: 'RS256'
628 });
629 const oauth2 = new JwtOAuth2({ loginUrl: options.loginUrl });
630 let _authFields;
631 try {
632 _authFields = ts_types_1.ensureJsonMap(await oauth2.jwtAuthorize(jwtToken));
633 }
634 catch (err) {
635 throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'JWTAuthError', [err.message]);
636 }
637 const authFields = {
638 accessToken: ts_types_1.asString(_authFields.access_token),
639 orgId: _parseIdUrl(ts_types_1.ensureString(_authFields.id)).orgId,
640 loginUrl: options.loginUrl,
641 privateKey: options.privateKey,
642 clientId: options.clientId
643 };
644 const instanceUrl = ts_types_1.ensureString(_authFields.instance_url);
645 const parsedUrl = url_1.parse(instanceUrl);
646 try {
647 // Check if the url is resolvable. This can fail when my-domains have not been replicated.
648 await this.lookup(ts_types_1.ensure(parsedUrl.hostname));
649 authFields.instanceUrl = instanceUrl;
650 }
651 catch (err) {
652 this.logger.debug(`Instance URL [${_authFields.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`);
653 authFields.instanceUrl = options.loginUrl;
654 }
655 return authFields;
656 }
657 // Build OAuth config for a refresh token auth flow
658 async buildRefreshTokenConfig(options) {
659 // Ideally, this would be removed at some point in the distant future when all auth files
660 // now have the clientId stored in it.
661 if (!options.clientId) {
662 options.clientId = DEFAULT_CONNECTED_APP_INFO.legacyClientId;
663 options.clientSecret = DEFAULT_CONNECTED_APP_INFO.legacyClientSecret;
664 }
665 const oauth2 = new jsforce_1.OAuth2(options);
666 let _authFields;
667 try {
668 _authFields = await oauth2.refreshToken(ts_types_1.ensure(options.refreshToken));
669 }
670 catch (err) {
671 throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'RefreshTokenAuthError', [err.message]);
672 }
673 return {
674 accessToken: _authFields.access_token,
675 // @ts-ignore TODO: need better typings for jsforce
676 instanceUrl: _authFields.instance_url,
677 // @ts-ignore TODO: need better typings for jsforce
678 orgId: _parseIdUrl(_authFields.id).orgId,
679 // @ts-ignore TODO: need better typings for jsforce
680 loginUrl: options.loginUrl || _authFields.instance_url,
681 refreshToken: options.refreshToken,
682 clientId: options.clientId,
683 clientSecret: options.clientSecret
684 };
685 }
686 /**
687 * Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge
688 * @param options The oauth options
689 * @param oauth2 The oauth2 extension that includes a code_challenge
690 */
691 async exchangeToken(options, oauth2 = new jsforce_1.OAuth2(options)) {
692 // Exchange the auth code for an access token and refresh token.
693 let _authFields;
694 try {
695 this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`);
696 _authFields = await oauth2.requestToken(ts_types_1.ensure(options.authCode));
697 }
698 catch (err) {
699 throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthCodeExchangeError', [err.message]);
700 }
701 // @ts-ignore TODO: need better typings for jsforce
702 const { userId, orgId } = _parseIdUrl(_authFields.id);
703 let username = this.getUsername();
704 // Only need to query for the username if it isn't known. For example, a new auth code exchange
705 // rather than refreshing a token on an existing connection.
706 if (!username) {
707 // Make a REST call for the username directly. Normally this is done via a connection
708 // but we don't want to create circular dependencies or lots of snowflakes
709 // within this file to support it.
710 const apiVersion = 'v42.0'; // hardcoding to v42.0 just for this call is okay.
711 const instance = ts_types_1.ensure(ts_types_1.getString(_authFields, 'instance_url'));
712 const url = `${instance}/services/data/${apiVersion}/sobjects/User/${userId}`;
713 const headers = Object.assign({ Authorization: `Bearer ${_authFields.access_token}` }, connection_1.SFDX_HTTP_HEADERS);
714 try {
715 this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${url}`);
716 const response = await new Transport().httpRequest({ url, headers });
717 username = ts_types_1.asString(kit_1.parseJsonMap(response.body).Username);
718 }
719 catch (err) {
720 throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthCodeUsernameRetrievalError', [orgId, err.message]);
721 }
722 }
723 return {
724 accessToken: _authFields.access_token,
725 // @ts-ignore TODO: need better typings for jsforce
726 instanceUrl: _authFields.instance_url,
727 orgId,
728 username,
729 // @ts-ignore TODO: need better typings for jsforce
730 loginUrl: options.loginUrl || _authFields.instance_url,
731 refreshToken: _authFields.refresh_token,
732 clientId: options.clientId,
733 clientSecret: options.clientSecret
734 };
735 }
736 // See https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback
737 async lookup(host) {
738 return new Promise((resolve, reject) => {
739 dns.lookup(host, (err, address, family) => {
740 if (err) {
741 reject(err);
742 }
743 else {
744 resolve({ address, family });
745 }
746 });
747 });
748 }
749}
750// The regular expression that filters files stored in $HOME/.sfdx
751AuthInfo.authFilenameFilterRegEx = /^[^.][^@]*@[^.]+(\.[^.\s]+)+\.json$/;
752// Cache of auth fields by username.
753AuthInfo.cache = new Map();
754exports.AuthInfo = AuthInfo;
755//# sourceMappingURL=authInfo.js.map
\No newline at end of file