@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
896 lines • 38.8 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.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