Skip to content

Commit

Permalink
Merge pull request #207 from forcedotcom/develop
Browse files Browse the repository at this point in the history
merge develop to master
  • Loading branch information
amphro authored Feb 11, 2020
2 parents 35570d2 + 9b89559 commit 70f997e
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 44 deletions.
107 changes: 70 additions & 37 deletions src/authInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,23 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
if (this.isTokenOptions(options)) {
authConfig = options;
} 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
});
}
}

// jwt flow
// Support both sfdx and jsforce private key values
if (!options.privateKey && options.privateKeyFile) {
Expand All @@ -673,24 +690,7 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
this.update(authConfig);
} else {
const username = ensure(this.getUsername());
if (AuthInfo.cache.has(username)) {
authConfig = ensure(AuthInfo.cache.get(username));
} else {
// Fetch from the persisted auth file
try {
const config: AuthInfoConfig = await AuthInfoConfig.create({
...AuthInfoConfig.getOptions(username),
throwOnNotFound: true
});
authConfig = config.toObject();
} catch (e) {
if (e.code === 'ENOENT') {
throw SfdxError.create('@salesforce/core', 'core', 'NamedOrgNotFound', [username]);
} else {
throw e;
}
}
}
authConfig = await this.loadAuthFromConfig(username);
// Update the auth fields WITHOUT encryption (already encrypted)
this.update(authConfig, false);
}
Expand All @@ -701,6 +701,27 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
return this;
}

private async loadAuthFromConfig(username: string): Promise<AuthFields> {
if (AuthInfo.cache.has(username)) {
return ensure(AuthInfo.cache.get(username));
} else {
// Fetch from the persisted auth file
try {
const config: AuthInfoConfig = await AuthInfoConfig.create({
...AuthInfoConfig.getOptions(username),
throwOnNotFound: true
});
return config.toObject();
} catch (e) {
if (e.code === 'ENOENT') {
throw SfdxError.create('@salesforce/core', 'core', 'NamedOrgNotFound', [username]);
} else {
throw e;
}
}
}
}

private isTokenOptions(options: OAuth2Options | AccessTokenOptions): options is AccessTokenOptions {
// 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.
Expand Down Expand Up @@ -781,9 +802,7 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
authFields.instanceUrl = instanceUrl;
} catch (err) {
this.logger.debug(
`Instance URL [${_authFields.instance_url}] is not available. DNS lookup failed. Using loginUrl [${
options.loginUrl
}] instead. This may result in a "Destination URL not reset" error.`
`Instance URL [${_authFields.instance_url}] is not available. DNS lookup failed. Using loginUrl [${options.loginUrl}] instead. This may result in a "Destination URL not reset" error.`
);
authFields.instanceUrl = options.loginUrl;
}
Expand Down Expand Up @@ -840,21 +859,26 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
// @ts-ignore TODO: need better typings for jsforce
const { userId, orgId } = _parseIdUrl(_authFields.id);

// 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 = 'v42.0'; // hardcoding to v42.0 just for this call is okay.
const instance = ensure(getString(_authFields, 'instance_url'));
const url = `${instance}/services/data/${apiVersion}/sobjects/User/${userId}`;
const headers = Object.assign({ Authorization: `Bearer ${_authFields.access_token}` }, SFDX_HTTP_HEADERS);

let username: Optional<string>;
try {
this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${url}`);
const response = await new Transport().httpRequest({ url, headers });
username = asString(parseJsonMap(response.body).Username);
} catch (err) {
throw SfdxError.create('@salesforce/core', 'core', 'AuthCodeUsernameRetrievalError', [orgId, err.message]);
let username: Optional<string> = 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) {
// 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 = 'v42.0'; // hardcoding to v42.0 just for this call is okay.
const instance = ensure(getString(_authFields, 'instance_url'));
const url = `${instance}/services/data/${apiVersion}/sobjects/User/${userId}`;
const headers = Object.assign({ Authorization: `Bearer ${_authFields.access_token}` }, SFDX_HTTP_HEADERS);

try {
this.logger.info(`Sending request for Username after successful auth code exchange to URL: ${url}`);
const response = await new Transport().httpRequest({ url, headers });
username = asString(parseJsonMap(response.body).Username);
} catch (err) {
throw SfdxError.create('@salesforce/core', 'core', 'AuthCodeUsernameRetrievalError', [orgId, err.message]);
}
}

return {
Expand All @@ -865,7 +889,9 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
username,
// @ts-ignore TODO: need better typings for jsforce
loginUrl: options.loginUrl || _authFields.instance_url,
refreshToken: _authFields.refresh_token
refreshToken: _authFields.refresh_token,
clientId: options.clientId,
clientSecret: options.clientSecret
};
}

Expand Down Expand Up @@ -902,5 +928,12 @@ export namespace AuthInfo {
accessTokenOptions?: AccessTokenOptions;

oauth2?: OAuth2;

/**
* In certain situations, a new auth info wants to use the connected app
* information from another parent org. Typically for scratch org or sandbox
* creation.
*/
parentUsername?: string;
}
}
73 changes: 66 additions & 7 deletions test/unit/authInfoTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,62 @@ describe('AuthInfo', () => {
expect(authInfo.isOauth(), 'authInfo.isOauth() should be false').to.be.false;
});

it('should return an AuthInfo instance when passed a parent username', async () => {
stubMethod($$.SANDBOX, ConfigAggregator.prototype, 'loadProperties').callsFake(async () => {});
stubMethod($$.SANDBOX, ConfigAggregator.prototype, 'getPropertyValue').returns(testMetadata.instanceUrl);

// Stub the http request (OAuth2.refreshToken())
// This will be called for both, and we want to make sure the clientSecrete is the
// same for both.
_postParmsStub.callsFake(params => {
expect(params.client_secret).to.deep.equal(testMetadata.clientSecret);
return {
access_token: testMetadata.accessToken,
instance_url: testMetadata.instanceUrl,
refresh_token: testMetadata.refreshToken,
id: '00DAuthInfoTest_orgId/005AuthInfoTest_userId'
};
});

const parentUsername = '[email protected]';
await AuthInfo.create({
username: parentUsername,
oauth2Options: {
clientId: testMetadata.clientId,
clientSecret: testMetadata.clientSecret,
loginUrl: testMetadata.instanceUrl,
authCode: testMetadata.authCode
}
});

const authInfo = await AuthInfo.create({
username: testMetadata.username,
parentUsername,
oauth2Options: {
loginUrl: testMetadata.instanceUrl,
authCode: testMetadata.authCode
}
});

expect(_postParmsStub.calledTwice).to.true;
expect(authInfo.isAccessTokenFlow(), 'authInfo.isAccessTokenFlow() should be false').to.be.false;
expect(authInfo.isRefreshTokenFlow(), 'authInfo.isRefreshTokenFlow() should be false').to.be.true;
expect(authInfo.isJwt(), 'authInfo.isJwt() should be false').to.be.false;
expect(authInfo.isOauth(), 'authInfo.isOauth() should be true').to.be.true;

const expectedAuthConfig = {
accessToken: testMetadata.accessToken,
instanceUrl: testMetadata.instanceUrl,
username: testMetadata.username,
orgId: '00DAuthInfoTest_orgId',
loginUrl: testMetadata.instanceUrl,
refreshToken: testMetadata.refreshToken,
clientId: testMetadata.clientId,
clientSecret: testMetadata.clientSecret
};
expect(authInfoUpdate.secondCall.args[0]).to.deep.equal(expectedAuthConfig);
});

it('should return an AuthInfo instance when passed an access token and instanceUrl for the access token flow', async () => {
stubMethod($$.SANDBOX, ConfigAggregator.prototype, 'loadProperties').callsFake(async () => {});
stubMethod($$.SANDBOX, ConfigAggregator.prototype, 'getPropertyValue').returns(testMetadata.instanceUrl);
Expand Down Expand Up @@ -918,6 +974,9 @@ describe('AuthInfo', () => {
// Create the refresh token AuthInfo instance
const authInfo = await AuthInfo.create({ oauth2Options: authCodeConfig });

// Ensure we query for the username
expect(Transport.prototype.httpRequest.called).to.be.true;

// Verify the returned AuthInfo instance
const authInfoConnOpts = authInfo.getConnectionOptions();
expect(authInfoConnOpts).to.have.property('accessToken', authResponse.access_token);
Expand Down Expand Up @@ -954,7 +1013,11 @@ describe('AuthInfo', () => {
username,
orgId: authResponse.id.split('/')[0],
loginUrl: authCodeConfig.loginUrl,
refreshToken: authResponse.refresh_token
refreshToken: authResponse.refresh_token,
// These need to be passed in by the consumer. Since they are not, they will show up as undefined.
// In a non-test environment, the exchange will fail because no clientId is supplied.
clientId: undefined,
clientSecret: undefined
};
expect(authInfoUpdate.firstCall.args[0]).to.deep.equal(expectedAuthConfig);
});
Expand Down Expand Up @@ -1298,9 +1361,7 @@ describe('AuthInfo', () => {
});

expect(authInfo.getSfdxAuthUrl()).to.contain(
`force://SalesforceDevelopmentExperience:1384510088588713504:${
testMetadata.refreshToken
}@mydevhub.localhost.internal.salesforce.com:6109`
`force://SalesforceDevelopmentExperience:1384510088588713504:${testMetadata.refreshToken}@mydevhub.localhost.internal.salesforce.com:6109`
);
});

Expand Down Expand Up @@ -1330,9 +1391,7 @@ describe('AuthInfo', () => {
delete authInfo.getFields().clientSecret;

expect(authInfo.getSfdxAuthUrl()).to.contain(
`force://SalesforceDevelopmentExperience::${
testMetadata.refreshToken
}@mydevhub.localhost.internal.salesforce.com:6109`
`force://SalesforceDevelopmentExperience::${testMetadata.refreshToken}@mydevhub.localhost.internal.salesforce.com:6109`
);
});
});
Expand Down

0 comments on commit 70f997e

Please sign in to comment.