@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
863 lines • 37.1 kB
JavaScript
"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.getJwtAudienceUrl = exports.SfdcUrl = exports.OAuth2WithVerifier = void 0;
/* eslint-disable @typescript-eslint/ban-ts-comment */
const crypto_1 = require("crypto");
const url_1 = require("url");
const dns = require("dns");
const path_1 = require("path");
const url_2 = require("url");
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 config_1 = require("../config/config");
const configAggregator_1 = require("../config/configAggregator");
const logger_1 = require("../logger");
const sfdxError_1 = require("../sfdxError");
const fs_1 = require("../util/fs");
const sfdc_1 = require("../util/sfdc");
const myDomainResolver_1 = require("../status/myDomainResolver");
const globalInfoConfig_1 = require("../config/globalInfoConfig");
const messages_1 = require("../messages");
const connection_1 = require("./connection");
const orgConfigProperties_1 = require("./orgConfigProperties");
messages_1.Messages.importMessagesDirectory(__dirname);
const messages = messages_1.Messages.load('@salesforce/core', 'core', [
'authInfoCreationError',
'authInfoOverwriteError',
'namedOrgNotFound',
'orgDataNotAvailableError',
'orgDataNotAvailableError.actions',
'refreshTokenAuthError',
'jwtAuthError',
'authCodeUsernameRetrievalError',
'authCodeExchangeError',
]);
// 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(true).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
*/
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;
/**
* Salesforce URLs.
*/
var SfdcUrl;
(function (SfdcUrl) {
SfdcUrl["SANDBOX"] = "https://test.salesforce.com";
SfdcUrl["PRODUCTION"] = "https://login.salesforce.com";
})(SfdcUrl = exports.SfdcUrl || (exports.SfdcUrl = {}));
function isSandboxUrl(options) {
var _a;
const createdOrgInstance = ts_types_1.getString(options, 'createdOrgInstance', '').trim().toLowerCase();
const loginUrl = (_a = options.loginUrl) !== null && _a !== void 0 ? _a : '';
return (/^cs|s$/gi.test(createdOrgInstance) ||
/sandbox\.my\.salesforce\.com/gi.test(loginUrl) || // enhanced domains >= 230
/(cs[0-9]+(\.my|)\.salesforce\.com)/gi.test(loginUrl) || // my domains on CS instance OR CS instance without my domain
/([a-z]{3}[0-9]+s\.sfdc-.+\.salesforce\.com)/gi.test(loginUrl) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com
/([a-z]{3}[0-9]+s\.sfdc-.+\.force\.com)/gi.test(loginUrl) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com
url_2.parse(loginUrl).hostname === 'test.salesforce.com');
}
async function resolvesToSandbox(options) {
if (isSandboxUrl(options)) {
return true;
}
let cnames = [];
if (options.loginUrl) {
const myDomainResolver = await myDomainResolver_1.MyDomainResolver.create({ url: new url_1.URL(options.loginUrl) });
cnames = await myDomainResolver.getCnames();
}
return cnames.some((cname) => isSandboxUrl({ ...options, loginUrl: cname }));
}
async function getJwtAudienceUrl(options) {
var _a;
// environment variable is used as an override
if (process.env.SFDX_AUDIENCE_URL) {
return process.env.SFDX_AUDIENCE_URL;
}
if (options.loginUrl && sfdc_1.sfdc.isInternalUrl(options.loginUrl)) {
// This is for internal developers when just doing authorize;
return options.loginUrl;
}
if (await resolvesToSandbox(options)) {
return SfdcUrl.SANDBOX;
}
const createdOrgInstance = ts_types_1.getString(options, 'createdOrgInstance', '').trim().toLowerCase();
if (/^gs1/gi.test(createdOrgInstance) || /(gs1.my.salesforce.com)/gi.test((_a = options.loginUrl) !== null && _a !== void 0 ? _a : '')) {
return 'https://gs1.salesforce.com';
}
return SfdcUrl.PRODUCTION;
}
exports.getJwtAudienceUrl = getJwtAudienceUrl;
// 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',
};
// 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.AsyncOptionalCreatable {
/**
* 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);
// 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.PRODUCTION;
}
/**
* Get a list of all authorizations based on auth files stored in the global directory.
*
* @returns {Promise<SfOrg[]>}
*/
static async listAllAuthorizations() {
const globalInfo = await globalInfoConfig_1.GlobalInfo.getInstance();
const auths = Object.values(globalInfo.getOrgs());
const aliases = await aliases_1.Aliases.create(aliases_1.Aliases.getDefaultOptions());
const final = [];
for (const auth of auths) {
const username = ts_types_1.ensureString(auth.username);
const [alias] = aliases.getKeysByValue(username);
try {
const authInfo = await AuthInfo.create({ username });
const { orgId, instanceUrl } = authInfo.getFields();
final.push({
alias,
username,
orgId,
instanceUrl,
accessToken: authInfo.getConnectionOptions().accessToken,
oauthMethod: authInfo.isJwt() ? 'jwt' : authInfo.isOauth() ? 'web' : 'token',
timestamp: auth.timestamp,
});
}
catch (err) {
final.push({
alias,
username,
orgId: auth.orgId,
instanceUrl: auth.instanceUrl,
accessToken: undefined,
oauthMethod: 'unknown',
error: err.message,
timestamp: auth.timestamp,
});
}
}
return final;
}
/**
* Returns true if one or more authentications are persisted.
*/
static async hasAuthentications() {
try {
const auths = (await globalInfoConfig_1.GlobalInfo.getInstance()).getOrgs();
return !kit_1.isEmpty(auths);
}
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);
}
/**
* 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>@<loginUrl>`. The instanceUrl must not have the protocol set.', 'INVALID_SFDX_AUTH_URL');
}
const [, clientId, clientSecret, refreshToken, loginUrl] = match;
return {
clientId,
clientSecret,
refreshToken,
loginUrl: `https://${loginUrl}`,
};
}
/**
* Get the username.
*/
getUsername() {
return this.username;
}
/**
* Returns true if `this` is using the JWT flow.
*/
isJwt() {
const { refreshToken, privateKey } = this.getFields();
return !refreshToken && !!privateKey;
}
/**
* Returns true if `this` is using an access token flow.
*/
isAccessTokenFlow() {
const { refreshToken, privateKey } = this.getFields();
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.getFields();
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;
}
await this.globalInfo.write();
this.logger.info(`Saved auth info for username: ${username}`);
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.
*/
update(authData) {
// todo move into configstore
if (authData && ts_types_1.isPlainObject(authData)) {
this.username = authData.username || this.username;
const existingFields = this.globalInfo.getOrg(this.getUsername());
const mergedFields = Object.assign({}, existingFields || {}, authData);
this.globalInfo.setOrg(this.getUsername(), mergedFields);
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 decryptedCopy = this.getFields(true);
const { accessToken, instanceUrl, loginUrl } = decryptedCopy;
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.PRODUCTION,
clientId: decryptedCopy.clientId || exports.DEFAULT_CONNECTED_APP_INFO.legacyClientId,
redirectUri: 'http://localhost:1717/OauthRedirect',
},
accessToken,
instanceUrl,
refreshFn: this.refreshFn.bind(this),
};
}
// decrypt the fields
return opts;
}
/**
* Get the authorization fields.
*
* @param decrypt Decrypt the fields.
*/
getFields(decrypt) {
return this.globalInfo.getOrg(this.username, decrypt);
}
/**
* Get the org front door (used for web based oauth flows)
*/
getOrgFrontDoorUrl() {
const authFields = this.getFields(true);
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.getFields(true);
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 target-env (default) or the target-dev-hub 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 = { org: true }) {
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.org) {
config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG, value);
}
if (options.devHub) {
config.set(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB, 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() {
// We have to set the global instance here because we need synchronous access to it later
this.globalInfo = await globalInfoConfig_1.GlobalInfo.getInstance();
const username = this.options.username;
const authOptions = this.options.oauth2Options || this.options.accessTokenOptions;
// Must specify either username and/or options
if (!username && !authOptions) {
throw messages.createError('authInfoCreationError');
}
// If a username AND oauth options, ensure an authorization for the username doesn't
// already exist. Throw if it does so we don't overwrite the authorization.
if (username && authOptions) {
const authExists = this.globalInfo.hasOrg(username);
if (authExists) {
throw messages.createError('authInfoOverwriteError');
}
}
const oauthUsername = username || ts_types_1.getString(authOptions, 'username');
if (oauthUsername) {
this.username = oauthUsername;
} // Else it will be set in initAuthOptions below.
// If the username is an access token, use that for auth and don't persist
if (ts_types_1.isString(oauthUsername) && sfdc_1.sfdc.matchesAccessToken(oauthUsername)) {
// Need to initAuthOptions the logger and authInfoCrypto since we don't call init()
this.logger = await logger_1.Logger.child('AuthInfo');
const aggregator = await configAggregator_1.ConfigAggregator.create();
const instanceUrl = this.getInstanceUrl(authOptions, aggregator);
this.update({
accessToken: oauthUsername,
instanceUrl,
orgId: oauthUsername.split('!')[0],
loginUrl: instanceUrl,
});
this.usingAccessToken = true;
}
// If a username with NO oauth options, ensure authorization already exist.
else if (username && !authOptions && !this.globalInfo.hasOrg(username)) {
throw messages.createError('namedOrgNotFound', [username]);
}
else {
await this.initAuthOptions(authOptions);
}
}
getInstanceUrl(options, aggregator) {
const instanceUrl = ts_types_1.getString(options, 'instanceUrl') || aggregator.getPropertyValue('instanceUrl');
return instanceUrl || 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: 'NamedOrgNotFoundError' }* Org information does not exist.
* @returns {Promise<AuthInfo>}
*/
async initAuthOptions(options) {
this.logger = await logger_1.Logger.child('AuthInfo');
// 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.update({ username: userInfo === null || userInfo === void 0 ? void 0 : userInfo.username, orgId: userInfo === null || userInfo === void 0 ? void 0 : userInfo.organizationId });
}
else {
if (this.options.parentUsername) {
const parentFields = await this.loadDecryptedAuthFromConfig(this.options.parentUsername);
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);
}
return this;
}
async loadDecryptedAuthFromConfig(username) {
// Fetch from the persisted auth file
const authInfo = this.globalInfo.getOrg(username, true);
if (!authInfo) {
throw messages.createError('namedOrgNotFound', [username]);
}
return authInfo;
}
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.getFields(true);
await this.initAuthOptions(fields);
await this.save();
return await callback(null, fields.accessToken);
}
catch (err) {
if (err.message && err.message.includes('Data Not Available')) {
// Set cause to keep original stacktrace
return await callback(messages.createError('orgDataNotAvailableError', [this.getUsername()], [], err));
}
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 audienceUrl = await getJwtAudienceUrl(options);
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 messages.createError('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 parsedUrl = url_2.parse(instanceUrl);
try {
// Check if the url is resolvable. This can fail when my-domains have not been replicated.
await this.lookup(ts_types_1.ensure(parsedUrl.hostname));
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 messages.createError('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 = ts_types_1.ensureString(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 messages.createError('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 url_1.URL(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 messages.createError('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 errorMsg = '';
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)) {
errorMsg = 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 {
errorMsg = (_a = ts_types_1.getString(body, 'message')) !== null && _a !== void 0 ? _a : ts_types_1.getString(body, 'errorCode', 'UNKNOWN');
}
}
catch (err) {
errorMsg = `${bodyAsString}`;
}
throw new sfdxError_1.SfdxError(errorMsg);
}
// See https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback
async lookup(host) {
return new Promise((resolve, reject) => {
dns.lookup(host, (err, address, family) => {
if (err) {
reject(err);
}
else {
resolve({ address, family });
}
});
});
}
}
exports.AuthInfo = AuthInfo;
//# sourceMappingURL=authInfo.js.map