Skip to content

Commit 99af2f0

Browse files
authored
Merge pull request #1228 from forcedotcom/sh/support-codebuilder-ca
feat: support code builder web auth - W-16646649
2 parents 2be205c + 60ebef1 commit 99af2f0

File tree

5 files changed

+73
-5
lines changed

5 files changed

+73
-5
lines changed

messages/auth.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Invalid request method: %s
2424

2525
Invalid request uri: %s
2626

27+
# warn.invalidClientId
28+
29+
Code Builder web authentication is only supported with the Code Builder connected app. Ignoring client ID: %s
30+
31+
# error.invalidCodeBuilderState
32+
33+
Invalid authentication state.
34+
2735
# error.HttpApi
2836

2937
HTTP response contains html content. Check that the org exists and can be reached.

src/org/authInfo.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export type JwtOAuth2Config = OAuth2Config & {
129129
authCode?: string;
130130
refreshToken?: string;
131131
username?: string;
132+
state?: string;
132133
};
133134

134135
type UserInfo = AnyJson & {
@@ -191,6 +192,11 @@ export const DEFAULT_CONNECTED_APP_INFO = {
191192
clientSecret: '',
192193
};
193194

195+
export const CODE_BUILDER_CONNECTED_APP_INFO = {
196+
clientId: 'CodeBuilder',
197+
clientSecret: '',
198+
};
199+
194200
/**
195201
* Handles persistence and fetching of user authentication information using
196202
* JWT, OAuth, or refresh tokens. Sets up the refresh flows that jsForce will
@@ -350,7 +356,7 @@ export class AuthInfo extends AsyncOptionalCreatable<AuthInfo.Options> {
350356
// The state parameter allows the redirectUri callback listener to ignore request
351357
// that don't contain the state value.
352358
const params = {
353-
state: randomBytes(Math.ceil(6)).toString('hex'),
359+
state: options.state ?? randomBytes(Math.ceil(6)).toString('hex'),
354360
prompt: 'login',
355361
// Default connected app is 'refresh_token api web'
356362
scope: options.scope ?? env.getString('SFDX_AUTH_SCOPES', 'refresh_token api web'),

src/webOAuthServer.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { OAuth2 } from '@jsforce/jsforce-node';
1616
import { AsyncCreatable, Env, set, toNumber } from '@salesforce/kit';
1717
import { asString, ensureString, get, Nullable } from '@salesforce/ts-types';
1818
import { Logger } from './logger/logger';
19-
import { AuthInfo, DEFAULT_CONNECTED_APP_INFO } from './org/authInfo';
19+
import { AuthInfo, DEFAULT_CONNECTED_APP_INFO, CODE_BUILDER_CONNECTED_APP_INFO } from './org/authInfo';
2020
import { SfError } from './sfError';
2121
import { Messages } from './messages';
2222
import { SfProjectJson } from './sfProject';
@@ -28,6 +28,8 @@ const messages = Messages.loadMessages('@salesforce/core', 'auth');
2828
// Server ignores requests for site icons
2929
const iconPaths = ['/favicon.ico', '/apple-touch-icon-precomposed.png'];
3030

31+
const CODE_BUILDER_REDIRECT_URI = 'https://api.code-builder.platform.salesforce.com/api/auth/salesforce/callback';
32+
3133
/**
3234
* Handles the creation of a web server for web based login flows.
3335
*
@@ -193,10 +195,28 @@ export class WebOAuthServer extends AsyncCreatable<WebOAuthServer.Options> {
193195
protected async init(): Promise<void> {
194196
this.logger = await Logger.child(this.constructor.name);
195197
const port = await WebOAuthServer.determineOauthPort();
198+
this.oauthConfig.loginUrl ??= AuthInfo.getDefaultInstanceUrl();
199+
const env = new Env();
200+
201+
if (env.getBoolean('CODE_BUILDER')) {
202+
if (this.oauthConfig.clientId && this.oauthConfig.clientId !== CODE_BUILDER_CONNECTED_APP_INFO.clientId) {
203+
this.logger.warn(messages.getMessage('warn.invalidClientId', [this.oauthConfig.clientId]));
204+
}
205+
this.oauthConfig.clientId = CODE_BUILDER_CONNECTED_APP_INFO.clientId;
206+
const cbStateSha = env.getString('CODE_BUILDER_STATE');
207+
if (!cbStateSha) {
208+
throw messages.createError('error.invalidCodeBuilderState');
209+
}
210+
this.oauthConfig.state = JSON.stringify({
211+
PORT: port,
212+
CODE_BUILDER_STATE: cbStateSha,
213+
});
214+
this.oauthConfig.redirectUri = CODE_BUILDER_REDIRECT_URI;
215+
} else {
216+
this.oauthConfig.clientId ??= DEFAULT_CONNECTED_APP_INFO.clientId;
217+
this.oauthConfig.redirectUri ??= `http://localhost:${port}/OauthRedirect`;
218+
}
196219

197-
if (!this.oauthConfig.clientId) this.oauthConfig.clientId = DEFAULT_CONNECTED_APP_INFO.clientId;
198-
if (!this.oauthConfig.loginUrl) this.oauthConfig.loginUrl = AuthInfo.getDefaultInstanceUrl();
199-
if (!this.oauthConfig.redirectUri) this.oauthConfig.redirectUri = `http://localhost:${port}/OauthRedirect`;
200220
// Unless explicitly turned off, use a code verifier as a best practice
201221
if (this.oauthConfig.useVerifier !== false) this.oauthConfig.useVerifier = true;
202222

test/unit/org/authInfo.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,24 @@ describe('AuthInfo', () => {
13021302
expect(url).to.contain('prompt=login');
13031303
expect(url).to.contain('scope=from-option');
13041304
});
1305+
1306+
it('should return the correct url with state', () => {
1307+
const state = JSON.stringify({ PORT: 12_345, CODE_BUILDER_STATE: '1234567890' });
1308+
const options = {
1309+
clientId: 'CodeBuilder',
1310+
redirectUri: testOrg.redirectUri,
1311+
loginUrl: testOrg.loginUrl,
1312+
state,
1313+
scope: 'test',
1314+
};
1315+
const url = AuthInfo.getAuthorizationUrl(options);
1316+
1317+
expect(url.startsWith(options.loginUrl), 'authorization URL should start with the loginUrl').to.be.true;
1318+
expect(url).to.contain(`state=${encodeURIComponent(state)}`);
1319+
expect(url).to.contain('prompt=login');
1320+
expect(url).to.contain('scope=test');
1321+
expect(url).to.contain('client_id=CodeBuilder');
1322+
});
13051323
});
13061324

13071325
describe('getSfdxAuthUrl', () => {

test/unit/webOauthServer.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ describe('WebOauthServer', () => {
5454
expect(authUrl).to.include('state=');
5555
expect(authUrl).to.include('response_type=code');
5656
});
57+
58+
it('should return authorization url for code builder', async () => {
59+
const cbState = '1234567890';
60+
stubMethod($$.SANDBOX, Env.prototype, 'getBoolean').withArgs('CODE_BUILDER').returns(true);
61+
stubMethod($$.SANDBOX, Env.prototype, 'getString').withArgs('CODE_BUILDER_STATE').returns(cbState);
62+
const oauthServer = await WebOAuthServer.create({ oauthConfig: {} });
63+
const authUrl = oauthServer.getAuthorizationUrl();
64+
expect(authUrl).to.not.be.undefined;
65+
expect(authUrl).to.include('client_id=CodeBuilder');
66+
expect(authUrl).to.include('code_challenge=');
67+
expect(authUrl).to.include('state=');
68+
expect(authUrl).to.include('response_type=code');
69+
expect(authUrl).to.include('redirect_uri=');
70+
expect(authUrl).to.include('api.code-builder.platform.salesforce.com');
71+
expect(authUrl).to.include(cbState);
72+
});
5773
});
5874

5975
describe('authorizeAndSave', () => {

0 commit comments

Comments
 (0)