UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

896 lines 38.8 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthInfo = exports.DEFAULT_CONNECTED_APP_INFO = exports.OAuth2WithVerifier = void 0; const crypto_1 = require("crypto"); const path_1 = require("path"); const path_2 = require("path"); const os = require("os"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const jsforce_1 = require("jsforce"); // No typings directly available for jsforce/lib/transport // @ts-ignore const Transport = require("jsforce/lib/transport"); const jwt = require("jsonwebtoken"); const aliases_1 = require("./config/aliases"); const authInfoConfig_1 = require("./config/authInfoConfig"); const config_1 = require("./config/config"); const configAggregator_1 = require("./config/configAggregator"); const connection_1 = require("./connection"); const crypto_2 = require("./crypto"); const global_1 = require("./global"); const logger_1 = require("./logger"); const sfdxError_1 = require("./sfdxError"); const fs_1 = require("./util/fs"); const sfdc_1 = require("./util/sfdc"); const sfdcUrl_1 = require("./util/sfdcUrl"); // Extend OAuth2 to add JWT Bearer Token Flow support. class JwtOAuth2 extends jsforce_1.OAuth2 { constructor(options) { super(options); } jwtAuthorize(innerToken, callback) { // @ts-ignore return super._postParams({ // eslint-disable-next-line camelcase grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: innerToken, }, callback); } } /** * Extend OAuth2 to add code verifier support for the auth code (web auth) flow * const oauth2 = new OAuth2WithVerifier({ loginUrl, clientSecret, clientId, redirectUri }); * * const authUrl = oauth2.getAuthorizationUrl({ * state: 'foo', * prompt: 'login', * scope: 'api web' * }); * console.log(authUrl); * const authCode = await retrieveCode(); * const authInfo = await AuthInfo.create({ oauth2Options: { clientId, clientSecret, loginUrl, authCode }, oauth2}); * console.log(`access token: ${authInfo.getFields().accessToken}`); */ class OAuth2WithVerifier extends jsforce_1.OAuth2 { constructor(options) { super(options); // Set a code verifier string for OAuth authorization this.codeVerifier = base64UrlEscape(crypto_1.randomBytes(Math.ceil(128)).toString('base64')); } /** * Overrides jsforce.OAuth2.getAuthorizationUrl. Get Salesforce OAuth2 authorization page * URL to redirect user agent, adding a verification code for added security. * * @param params */ getAuthorizationUrl(params) { // code verifier must be a base 64 url encoded hash of 128 bytes of random data. Our random data is also // base 64 url encoded. See Connection.create(); const codeChallenge = base64UrlEscape(crypto_1.createHash('sha256').update(this.codeVerifier).digest('base64')); kit_1.set(params, 'code_challenge', codeChallenge); return super.getAuthorizationUrl(params); } async requestToken(code, callback) { return super.requestToken(code, callback); } /** * Overrides jsforce.OAuth2._postParams because jsforce's oauth impl doesn't support * coder_verifier and code_challenge. This enables the server to disallow trading a one-time auth code * for an access/refresh token when the verifier and challenge are out of alignment. * * See https://github.com/jsforce/jsforce/issues/665 */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async _postParams(params, callback) { kit_1.set(params, 'code_verifier', this.codeVerifier); // @ts-ignore TODO: need better typings for jsforce return super._postParams(params, callback); } } exports.OAuth2WithVerifier = OAuth2WithVerifier; // parses the id field returned from jsForce oauth2 methods to get // user ID and org ID. function parseIdUrl(idUrl) { const idUrls = idUrl.split('/'); const userId = idUrls.pop(); const orgId = idUrls.pop(); return { userId, orgId, url: idUrl, }; } exports.DEFAULT_CONNECTED_APP_INFO = { clientId: 'PlatformCLI', // Legacy. The connected app info is owned by the thing that // creates new AuthInfos. Currently that is the auth:* commands which // aren't owned by this core library. These values need to be here // for any old auth files where the id and secret aren't stored. // // Ideally, this would be removed at some point in the distant future // when all auth files now have the clientId stored in it. legacyClientId: 'SalesforceDevelopmentExperience', legacyClientSecret: '1384510088588713504', }; class AuthInfoCrypto extends crypto_2.Crypto { decryptFields(fields) { return this.crypt(fields, 'decrypt'); } encryptFields(fields) { return this.crypt(fields, 'encrypt'); } crypt(fields, method) { const copy = {}; for (const key of ts_types_1.keysOf(fields)) { const rawValue = fields[key]; if (rawValue !== undefined) { if (ts_types_1.isString(rawValue) && AuthInfoCrypto.encryptedFields.includes(key)) { copy[key] = this[method](ts_types_1.asString(rawValue)); } else { copy[key] = rawValue; } } } return copy; } } AuthInfoCrypto.encryptedFields = [ 'accessToken', 'refreshToken', 'password', 'clientSecret', ]; // Makes a nodejs base64 encoded string compatible with rfc4648 alternative encoding for urls. // @param base64Encoded a nodejs base64 encoded string function base64UrlEscape(base64Encoded) { // builtin node js base 64 encoding is not 64 url compatible. // See https://toolsn.ietf.org/html/rfc4648#section-5 return base64Encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } /** * Handles persistence and fetching of user authentication information using * JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will * use to keep tokens active. An AuthInfo can also be created with an access * token, but AuthInfos created with access tokens can't be persisted to disk. * * **See** [Authorization](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth.htm) * * **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) * * ``` * // Creating a new authentication file. * const authInfo = await AuthInfo.create({ * username: myAdminUsername, * oauth2Options: { * loginUrl, authCode, clientId, clientSecret * } * ); * authInfo.save(); * * // Creating an authorization info with an access token. * const authInfo = await AuthInfo.create({ * username: accessToken * }); * * // Using an existing authentication file. * const authInfo = await AuthInfo.create({ * username: myAdminUsername * }); * * // Using the AuthInfo * const connection = await Connection.create({ authInfo }); * ``` */ class AuthInfo extends kit_1.AsyncCreatable { /** * Constructor * **Do not directly construct instances of this class -- use {@link AuthInfo.create} instead.** * * @param options The options for the class instance */ constructor(options) { super(options); // All sensitive fields are encrypted this.fields = {}; // Possibly overridden in create this.usingAccessToken = false; this.options = options; } /** * Returns the default instance url * * @returns {string} */ static getDefaultInstanceUrl() { const configuredInstanceUrl = configAggregator_1.ConfigAggregator.getValue('instanceUrl').value; return configuredInstanceUrl || sfdcUrl_1.SfdcUrl.PRODUCTION; } /** * Get a list of all auth files stored in the global directory. * * @returns {Promise<string[]>} * * @deprecated Removed in v3 {@link https://github.com/forcedotcom/sfdx-core/blob/v3/MIGRATING_V2-V3.md#globalinfo} */ static async listAllAuthFiles() { const globalFiles = await fs_1.fs.readdir(global_1.Global.DIR); const authFiles = globalFiles.filter((file) => file.match(AuthInfo.authFilenameFilterRegEx)); // Want to throw a clean error if no files are found. if (kit_1.isEmpty(authFiles)) { const errConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'NoAuthInfoFound'); throw sfdxError_1.SfdxError.create(errConfig); } // At least one auth file is in the global dir. return authFiles; } /** * Get a list of all authorizations based on auth files stored in the global directory. * * @returns {Promise<Authorization[]>} */ static async listAllAuthorizations() { const filenames = await AuthInfo.listAllAuthFiles(); const auths = []; const aliases = await aliases_1.Aliases.create(aliases_1.Aliases.getDefaultOptions()); for (const filename of filenames) { const username = path_2.basename(filename, path_2.extname(filename)); try { const config = await AuthInfo.create({ username }); const fields = config.getFields(); const usernameAliases = aliases.getKeysByValue(username); auths.push({ alias: usernameAliases[0], username: fields.username, orgId: fields.orgId, instanceUrl: fields.instanceUrl, accessToken: config.getConnectionOptions().accessToken, oauthMethod: config.isJwt() ? 'jwt' : config.isOauth() ? 'web' : 'token', }); } catch (err) { // Most likely, an error decrypting the token const file = await authInfoConfig_1.AuthInfoConfig.create(authInfoConfig_1.AuthInfoConfig.getOptions(username)); const contents = file.getContents(); const usernameAliases = aliases.getKeysByValue(contents.username); auths.push({ alias: usernameAliases[0], username: contents.username, orgId: contents.orgId, instanceUrl: contents.instanceUrl, accessToken: undefined, oauthMethod: 'unknown', error: err.message, }); } } return auths; } /** * Returns true if one or more authentications are persisted. */ static async hasAuthentications() { try { const authFiles = await this.listAllAuthFiles(); return !kit_1.isEmpty(authFiles); } catch (err) { if (err.name === 'OrgDataNotAvailableError' || err.code === 'ENOENT') { return false; } throw err; } } /** * Get the authorization URL. * * @param options The options to generate the URL. */ static getAuthorizationUrl(options, oauth2) { const oauth2Verifier = oauth2 || new OAuth2WithVerifier(options); // The state parameter allows the redirectUri callback listener to ignore request // that don't contain the state value. const params = { state: crypto_1.randomBytes(Math.ceil(6)).toString('hex'), prompt: 'login', // Default connected app is 'refresh_token api web' scope: options.scope || kit_1.env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'), }; return oauth2Verifier.getAuthorizationUrl(params); } /** * Forces the auth file to be re-read from disk for a given user. Returns `true` if a value was removed. * * @param username The username for the auth info to re-read. * * @deprecated Removed in v3 {@link https://github.com/forcedotcom/sfdx-core/blob/v3/MIGRATING_V2-V3.md#configstore-configfile-authinfo-and-encrypting-values} */ static clearCache(username) { if (username) { return AuthInfo.cache.delete(username); } return false; } /** * Parse a sfdx auth url, usually obtained by `authInfo.getSfdxAuthUrl`. * * @example * ``` * await AuthInfo.create(AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl)); * ``` * @param sfdxAuthUrl */ static parseSfdxAuthUrl(sfdxAuthUrl) { const match = sfdxAuthUrl.match(/^force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)/); if (!match) { throw new sfdxError_1.SfdxError('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'); } const [, clientId, clientSecret, refreshToken, loginUrl] = match; return { clientId, clientSecret, refreshToken, loginUrl: `https://${loginUrl}`, }; } /** * Get the username. */ getUsername() { return this.fields.username; } /** * Returns true if `this` is using the JWT flow. */ isJwt() { const { refreshToken, privateKey } = this.fields; return !refreshToken && !!privateKey; } /** * Returns true if `this` is using an access token flow. */ isAccessTokenFlow() { const { refreshToken, privateKey } = this.fields; return !refreshToken && !privateKey; } /** * Returns true if `this` is using the oauth flow. */ isOauth() { return !this.isAccessTokenFlow() && !this.isJwt(); } /** * Returns true if `this` is using the refresh token flow. */ isRefreshTokenFlow() { const { refreshToken, authCode } = this.fields; return !authCode && !!refreshToken; } /** * Updates the cache and persists the authentication fields (encrypted). * * @param authData New data to save. */ async save(authData) { this.update(authData); const username = ts_types_1.ensure(this.getUsername()); if (sfdc_1.sfdc.matchesAccessToken(username)) { this.logger.debug('Username is an accesstoken. Skip saving authinfo to disk.'); return this; } AuthInfo.cache.set(username, this.fields); const dataToSave = kit_1.cloneJson(this.fields); this.logger.debug(dataToSave); const config = await authInfoConfig_1.AuthInfoConfig.create({ ...authInfoConfig_1.AuthInfoConfig.getOptions(username), throwOnNotFound: false, }); config.setContentsFromObject(dataToSave); await config.write(); this.logger.info(`Saved auth info for username: ${this.getUsername()}`); return this; } /** * Update the authorization fields, encrypting sensitive fields, but do not persist. * For convenience `this` object is returned. * * @param authData Authorization fields to update. * @param encrypt Encrypt the fields. */ update(authData, encrypt = true) { if (authData && ts_types_1.isPlainObject(authData)) { let copy = kit_1.cloneJson(authData); if (encrypt) { copy = this.authInfoCrypto.encryptFields(copy); } Object.assign(this.fields, copy); this.logger.info(`Updated auth info for username: ${this.getUsername()}`); } return this; } /** * Get the auth fields (decrypted) needed to make a connection. */ getConnectionOptions() { let opts; const { accessToken, instanceUrl, loginUrl } = this.fields; if (this.isAccessTokenFlow()) { this.logger.info('Returning fields for a connection using access token.'); // Just auth with the accessToken opts = { accessToken, instanceUrl, loginUrl }; } else if (this.isJwt()) { this.logger.info('Returning fields for a connection using JWT config.'); opts = { accessToken, instanceUrl, refreshFn: this.refreshFn.bind(this), }; } else { // @TODO: figure out loginUrl and redirectUri (probably get from config class) // // redirectUri: org.config.getOauthCallbackUrl() // loginUrl: this.fields.instanceUrl || this.config.getAppConfig().sfdcLoginUrl this.logger.info('Returning fields for a connection using OAuth config.'); // Decrypt a user provided client secret or use the default. opts = { oauth2: { loginUrl: instanceUrl || sfdcUrl_1.SfdcUrl.PRODUCTION, clientId: this.fields.clientId || exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId, redirectUri: 'http://localhost:1717/OauthRedirect', }, accessToken, instanceUrl, refreshFn: this.refreshFn.bind(this), }; } // decrypt the fields return this.authInfoCrypto.decryptFields(opts); } /** * Get the authorization fields. * * @param decrypt Decrypt the fields. */ getFields(decrypt) { return decrypt ? this.authInfoCrypto.decryptFields(this.fields) : this.fields; } /** * Get the org front door (used for web based oauth flows) */ getOrgFrontDoorUrl() { const authFields = this.getFields(); const base = ts_types_1.ensureString(authFields.instanceUrl).replace(/\/+$/, ''); const accessToken = ts_types_1.ensureString(authFields.accessToken); return `${base}/secur/frontdoor.jsp?sid=${accessToken}`; } /** * Returns true if this org is using access token auth. */ isUsingAccessToken() { return this.usingAccessToken; } /** * Get the SFDX Auth URL. * * **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) */ getSfdxAuthUrl() { const decryptedFields = this.authInfoCrypto.decryptFields(this.fields); const instanceUrl = ts_types_1.ensure(decryptedFields.instanceUrl, 'undefined instanceUrl').replace(/^https?:\/\//, ''); let sfdxAuthUrl = 'force://'; if (decryptedFields.clientId) { sfdxAuthUrl += `${decryptedFields.clientId}:${decryptedFields.clientSecret || ''}:`; } sfdxAuthUrl += `${ts_types_1.ensure(decryptedFields.refreshToken, 'undefined refreshToken')}@${instanceUrl}`; return sfdxAuthUrl; } /** * Set the defaultusername or the defaultdevhubusername to the alias if * it exists otherwise to the username. Method will try to set the local * config first but will default to global config if that fails. * * @param options */ async setAsDefault(options) { let config; // if we fail to create the local config, default to the global config try { config = await config_1.Config.create({ isGlobal: false }); } catch { config = await config_1.Config.create({ isGlobal: true }); } const username = ts_types_1.ensureString(this.getUsername()); const aliases = await aliases_1.Aliases.create(aliases_1.Aliases.getDefaultOptions()); const value = aliases.getKeysByValue(username)[0] || username; if (options.defaultUsername) { config.set(config_1.Config.DEFAULT_USERNAME, value); } if (options.defaultDevhubUsername) { config.set(config_1.Config.DEFAULT_DEV_HUB_USERNAME, value); } await config.write(); } /** * Sets the provided alias to the username * * @param alias alias to set */ async setAlias(alias) { const username = this.getUsername(); await aliases_1.Aliases.parseAndUpdate([`${alias}=${username}`]); } /** * Initializes an instance of the AuthInfo class. */ async init() { // Must specify either username and/or options const options = this.options.oauth2Options || this.options.accessTokenOptions; if (!this.options.username && !(this.options.oauth2Options || this.options.accessTokenOptions)) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthInfoCreationError'); } // If a username AND oauth options were passed, ensure an auth file for the username doesn't // already exist. Throw if it does so we don't overwrite the auth file. if (this.options.username && this.options.oauth2Options) { const authInfoConfig = await authInfoConfig_1.AuthInfoConfig.create({ ...authInfoConfig_1.AuthInfoConfig.getOptions(this.options.username), throwOnNotFound: false, }); if (await authInfoConfig.exists()) { throw sfdxError_1.SfdxError.create(new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'AuthInfoOverwriteError', undefined, 'AuthInfoOverwriteErrorAction')); } } this.fields.username = this.options.username || ts_types_1.getString(options, 'username') || undefined; // If the username is an access token, use that for auth and don't persist if (ts_types_1.isString(this.fields.username) && sfdc_1.sfdc.matchesAccessToken(this.fields.username)) { // Need to initAuthOptions the logger and authInfoCrypto since we don't call init() this.logger = await logger_1.Logger.child('AuthInfo'); this.authInfoCrypto = await AuthInfoCrypto.create({ noResetOnClose: true, }); const aggregator = await configAggregator_1.ConfigAggregator.create(); const instanceUrl = this.getInstanceUrl(options, aggregator); this.update({ accessToken: this.options.username, instanceUrl, loginUrl: instanceUrl, orgId: this.fields.username.split('!')[0], }); this.usingAccessToken = true; } else { await this.initAuthOptions(options); } } getInstanceUrl(options, aggregator) { const instanceUrl = ts_types_1.getString(options, 'instanceUrl') || aggregator.getPropertyValue('instanceUrl'); return instanceUrl || sfdcUrl_1.SfdcUrl.PRODUCTION; } /** * Initialize this AuthInfo instance with the specified options. If options are not provided, initialize it from cache * or by reading from the persistence store. For convenience `this` object is returned. * * @param options Options to be used for creating an OAuth2 instance. * * **Throws** *{@link SfdxError}{ name: 'NamedOrgNotFound' }* Org information does not exist. * @returns {Promise<AuthInfo>} */ async initAuthOptions(options) { this.logger = await logger_1.Logger.child('AuthInfo'); this.authInfoCrypto = await AuthInfoCrypto.create(); // If options were passed, use those before checking cache and reading an auth file. let authConfig; if (options) { options = kit_1.cloneJson(options); if (this.isTokenOptions(options)) { authConfig = options; const userInfo = await this.retrieveUserInfo(ts_types_1.ensureString(options.instanceUrl), ts_types_1.ensureString(options.accessToken)); this.fields.username = userInfo === null || userInfo === void 0 ? void 0 : userInfo.username; this.fields.orgId = userInfo === null || userInfo === void 0 ? void 0 : userInfo.organizationId; } else { if (this.options.parentUsername) { const parentUserFields = await this.loadAuthFromConfig(this.options.parentUsername); const parentFields = this.authInfoCrypto.decryptFields(parentUserFields); options.clientId = parentFields.clientId; if (process.env.SFDX_CLIENT_SECRET) { options.clientSecret = process.env.SFDX_CLIENT_SECRET; } else { // Grab whatever flow is defined Object.assign(options, { clientSecret: parentFields.clientSecret, privateKey: parentFields.privateKey ? path_1.resolve(parentFields.privateKey) : parentFields.privateKey, }); } } // jwt flow // Support both sfdx and jsforce private key values if (!options.privateKey && options.privateKeyFile) { options.privateKey = path_1.resolve(options.privateKeyFile); } if (options.privateKey) { authConfig = await this.buildJwtConfig(options); } else if (!options.authCode && options.refreshToken) { // refresh token flow (from sfdxUrl or OAuth refreshFn) authConfig = await this.buildRefreshTokenConfig(options); } else { if (this.options.oauth2 instanceof OAuth2WithVerifier) { // authcode exchange / web auth flow authConfig = await this.exchangeToken(options, this.options.oauth2); } else { authConfig = await this.exchangeToken(options); } } } // Update the auth fields WITH encryption this.update(authConfig); } else { authConfig = await this.loadAuthFromConfig(ts_types_1.ensure(this.getUsername())); // Update the auth fields WITHOUT encryption (already encrypted) this.update(authConfig, false); } const username = this.getUsername(); if (username) { // Cache the fields by username (fields are encrypted) AuthInfo.cache.set(username, this.fields); } return this; } async loadAuthFromConfig(username) { if (AuthInfo.cache.has(username)) { return ts_types_1.ensure(AuthInfo.cache.get(username)); } else { // Fetch from the persisted auth file try { const config = await authInfoConfig_1.AuthInfoConfig.create({ ...authInfoConfig_1.AuthInfoConfig.getOptions(username), throwOnNotFound: true, }); return config.toObject(); } catch (e) { if (e.code === 'ENOENT') { throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'NamedOrgNotFound', [ this.options.isDevHub ? 'devhub username' : 'username', username, ]); } else { throw e; } } } } isTokenOptions(options) { // Although OAuth2Options does not contain refreshToken, privateKey, or privateKeyFile, a JS consumer could still pass those in // which WILL have an access token as well, but it should be considered an OAuth2Options at that point. return ('accessToken' in options && !('refreshToken' in options) && !('privateKey' in options) && !('privateKeyFile' in options) && !('authCode' in options)); } // A callback function for a connection to refresh an access token. This is used // both for a JWT connection and an OAuth connection. async refreshFn(conn, callback) { this.logger.info('Access token has expired. Updating...'); try { const fields = this.authInfoCrypto.decryptFields(this.fields); await this.initAuthOptions(fields); await this.save(); return await callback(null, fields.accessToken); } catch (err) { if (err.message && err.message.includes('Data Not Available')) { const errConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'core', 'OrgDataNotAvailableError', [ this.getUsername(), ]); for (let i = 1; i < 5; i++) { errConfig.addAction(`OrgDataNotAvailableErrorAction${i}`); } return await callback(sfdxError_1.SfdxError.create(errConfig)); } return await callback(err); } } // Build OAuth config for a JWT auth flow async buildJwtConfig(options) { const privateKeyContents = await fs_1.fs.readFile(ts_types_1.ensure(options.privateKey), 'utf8'); const { loginUrl = sfdcUrl_1.SfdcUrl.PRODUCTION } = options; const url = new sfdcUrl_1.SfdcUrl(loginUrl); const createdOrgInstance = ts_types_1.getString(options, 'createdOrgInstance', '').trim().toLowerCase(); const audienceUrl = await url.getJwtAudienceUrl(createdOrgInstance); const jwtToken = jwt.sign({ iss: options.clientId, sub: this.getUsername(), aud: audienceUrl, exp: Date.now() + 300, }, privateKeyContents, { algorithm: 'RS256', }); const oauth2 = new JwtOAuth2({ loginUrl: options.loginUrl }); let authFieldsBuilder; try { authFieldsBuilder = ts_types_1.ensureJsonMap(await oauth2.jwtAuthorize(jwtToken)); } catch (err) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'JWTAuthError', [err.message]); } const authFields = { accessToken: ts_types_1.asString(authFieldsBuilder.access_token), orgId: parseIdUrl(ts_types_1.ensureString(authFieldsBuilder.id)).orgId, loginUrl: options.loginUrl, privateKey: options.privateKey, clientId: options.clientId, }; const instanceUrl = ts_types_1.ensureString(authFieldsBuilder.instance_url); const sfdcUrl = new sfdcUrl_1.SfdcUrl(instanceUrl); try { // Check if the url is resolvable. This can fail when my-domains have not been replicated. await sfdcUrl.lookup(); authFields.instanceUrl = instanceUrl; } catch (err) { this.logger.debug(`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.`); authFields.instanceUrl = options.loginUrl; } return authFields; } // Build OAuth config for a refresh token auth flow async buildRefreshTokenConfig(options) { // Ideally, this would be removed at some point in the distant future when all auth files // now have the clientId stored in it. if (!options.clientId) { options.clientId = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId; options.clientSecret = exports.DEFAULT_CONNECTED_APP_INFO.legacyClientSecret; } const oauth2 = new jsforce_1.OAuth2(options); let authFieldsBuilder; try { authFieldsBuilder = await oauth2.refreshToken(ts_types_1.ensure(options.refreshToken)); } catch (err) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'RefreshTokenAuthError', [err.message]); } // @ts-ignore const { orgId } = parseIdUrl(authFieldsBuilder.id); let username = this.getUsername(); if (!username) { // @ts-ignore const userInfo = await this.retrieveUserInfo(authFieldsBuilder.instance_url, authFieldsBuilder.access_token); username = userInfo === null || userInfo === void 0 ? void 0 : userInfo.username; } return { orgId, username, accessToken: authFieldsBuilder.access_token, // @ts-ignore TODO: need better typings for jsforce instanceUrl: authFieldsBuilder.instance_url, // @ts-ignore TODO: need better typings for jsforce loginUrl: options.loginUrl || authFieldsBuilder.instance_url, refreshToken: options.refreshToken, clientId: options.clientId, clientSecret: options.clientSecret, }; } /** * Performs an authCode exchange but the Oauth2 feature of jsforce is extended to include a code_challenge * * @param options The oauth options * @param oauth2 The oauth2 extension that includes a code_challenge */ async exchangeToken(options, oauth2 = new jsforce_1.OAuth2(options)) { // Exchange the auth code for an access token and refresh token. let authFields; try { this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`); authFields = await oauth2.requestToken(ts_types_1.ensure(options.authCode)); } catch (err) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthCodeExchangeError', [err.message]); } // @ts-ignore TODO: need better typings for jsforce const { orgId } = parseIdUrl(authFields.id); let username = this.getUsername(); // Only need to query for the username if it isn't known. For example, a new auth code exchange // rather than refreshing a token on an existing connection. if (!username) { // @ts-ignore const userInfo = await this.retrieveUserInfo(authFields.instance_url, authFields.access_token); username = userInfo === null || userInfo === void 0 ? void 0 : userInfo.username; } return { accessToken: authFields.access_token, // @ts-ignore TODO: need better typings for jsforce instanceUrl: authFields.instance_url, orgId, username, // @ts-ignore TODO: need better typings for jsforce loginUrl: options.loginUrl || authFields.instance_url, refreshToken: authFields.refresh_token, clientId: options.clientId, clientSecret: options.clientSecret, }; } async retrieveUserInfo(instanceUrl, accessToken) { // Make a REST call for the username directly. Normally this is done via a connection // but we don't want to create circular dependencies or lots of snowflakes // within this file to support it. const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay. const instance = ts_types_1.ensure(instanceUrl); const baseUrl = new sfdcUrl_1.SfdcUrl(instance); const userInfoUrl = `${baseUrl}services/oauth2/userinfo`; const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, connection_1.SFDX_HTTP_HEADERS); try { this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${userInfoUrl}`); let response = await new Transport().httpRequest({ url: userInfoUrl, headers }); if (response.statusCode >= 400) { this.throwUserGetException(response); } else { const userInfoJson = kit_1.parseJsonMap(response.body); const url = `${baseUrl}/services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`; this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`); response = await new Transport().httpRequest({ url, headers }); if (response.statusCode >= 400) { this.throwUserGetException(response); } else { // eslint-disable-next-line camelcase userInfoJson.preferred_username = kit_1.parseJsonMap(response.body).Username; } return { username: userInfoJson.preferred_username, organizationId: userInfoJson.organization_id }; } } catch (err) { throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'AuthCodeUsernameRetrievalError', [err.message]); } } /** * Given an error while getting the User object, handle different possibilities of response.body. * * @param response * @private */ throwUserGetException(response) { var _a; let messages = ''; const bodyAsString = ts_types_1.getString(response, 'body', JSON.stringify({ message: 'UNKNOWN', errorCode: 'UNKNOWN' })); try { const body = kit_1.parseJson(bodyAsString); if (ts_types_1.isArray(body)) { messages = body .map((line) => { var _a; return (_a = ts_types_1.getString(line, 'message')) !== null && _a !== void 0 ? _a : ts_types_1.getString(line, 'errorCode', 'UNKNOWN'); }) .join(os.EOL); } else { messages = (_a = ts_types_1.getString(body, 'message')) !== null && _a !== void 0 ? _a : ts_types_1.getString(body, 'errorCode', 'UNKNOWN'); } } catch (err) { messages = `${bodyAsString}`; } throw new sfdxError_1.SfdxError(messages); } } exports.AuthInfo = AuthInfo; // The regular expression that filters files stored in $HOME/.sfdx AuthInfo.authFilenameFilterRegEx = /^[^.][^@]*@[^.]+(\.[^.\s]+)+\.json$/; // Cache of auth fields by username. AuthInfo.cache = new Map(); //# sourceMappingURL=authInfo.js.map