Skip to content

Commit 1f29721

Browse files
authored
feat: establish valid experience site sessions at startup (#175)
1 parent 809b895 commit 1f29721

File tree

4 files changed

+138
-2
lines changed

4 files changed

+138
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@salesforce/sf-plugins-core": "^11.2.4",
1616
"@inquirer/select": "^2.4.7",
1717
"@inquirer/prompts": "^5.3.8",
18+
"axios": "^1.7.7",
1819
"chalk": "^5.3.0",
1920
"lwc": "7.1.3",
2021
"lwr": "0.14.3",

src/commands/lightning/dev/site.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ export default class LightningDevSite extends SfCommand<void> {
6464
}
6565
}
6666

67-
// Pass the org auth token so LWR can make authenticated requests to core
68-
const authToken = org.getConnection().accessToken ?? '';
67+
// Establish a valid access token for this site
68+
const authToken = await selectedSite.setupAuth();
6969

7070
// Start the dev server
7171
await expDev({

src/shared/experience/expSite.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import fs from 'node:fs';
88
import path from 'node:path';
99
import { Org, SfError } from '@salesforce/core';
10+
import axios from 'axios';
1011

1112
export type SiteMetadata = {
1213
bundleName: string;
@@ -94,6 +95,28 @@ export class ExperienceSite {
9495
return experienceSites;
9596
}
9697

98+
/**
99+
* Esablish a valid token for this local development session
100+
*
101+
* @returns sid token for proxied site requests
102+
*/
103+
public async setupAuth(): Promise<string> {
104+
let sidToken = ''; // Default to guest user access only
105+
106+
// Use environment variable for now if users want to just have guest access only
107+
if (process.env.SITE_GUEST_ACCESS !== 'true') {
108+
try {
109+
const networkId = await this.getNetworkId();
110+
sidToken = await this.getNewSidToken(networkId);
111+
} catch (e) {
112+
// eslint-disable-next-line no-console
113+
console.error('Failed to establish authentication for site', e);
114+
}
115+
}
116+
117+
return sidToken;
118+
}
119+
97120
public async isUpdateAvailable(): Promise<boolean> {
98121
const localMetadata = this.getLocalMetadata();
99122
if (!localMetadata) {
@@ -225,6 +248,99 @@ export class ExperienceSite {
225248

226249
return resourcePath;
227250
}
251+
252+
private async getNetworkId(): Promise<string> {
253+
const conn = this.org.getConnection();
254+
// Query the Network object for the network with the given site name
255+
const result = await conn.query<{ Id: string }>(`SELECT Id FROM Network WHERE Name = '${this.siteDisplayName}'`);
256+
257+
const record = result.records[0];
258+
if (record) {
259+
let networkId = record.Id;
260+
// Subtract the last three characters from the Network ID
261+
networkId = networkId.substring(0, networkId.length - 3);
262+
return networkId;
263+
} else {
264+
throw new Error(`NetworkId for site: '${this.siteDisplayName}' could not be found`);
265+
}
266+
}
267+
268+
private async getNewSidToken(networkId: string): Promise<string> {
269+
// Get the connection and access token from the org
270+
const conn = this.org.getConnection();
271+
const orgId = this.org.getOrgId();
272+
273+
// Not sure if we need to do this
274+
const orgIdMinus3 = orgId.substring(0, orgId.length - 3);
275+
const accessToken = conn.accessToken;
276+
const instanceUrl = conn.instanceUrl; // Org URL
277+
278+
// Make the GET request without following redirects
279+
if (accessToken) {
280+
// TODO should we try and refresh auth here?
281+
// await conn.refreshAuth();
282+
283+
// Call out to the switcher servlet to establish a session
284+
const switchUrl = `${instanceUrl}/servlet/networks/switch?networkId=${networkId}`;
285+
const cookies = [`sid=${accessToken}`, `oid=${orgIdMinus3}`].join('; ').trim();
286+
let response = await axios.get(switchUrl, {
287+
headers: {
288+
Cookie: cookies,
289+
},
290+
withCredentials: true,
291+
maxRedirects: 0, // Prevent axios from following redirects
292+
validateStatus: (status) => status >= 200 && status < 400, // Accept 3xx status codes
293+
});
294+
295+
// Extract the Location callback header
296+
const locationHeader = response.headers['location'] as string;
297+
if (locationHeader) {
298+
// Parse the URL to extract the 'sid' parameter
299+
const urlObj = new URL(locationHeader);
300+
const sid = urlObj.searchParams.get('sid') ?? '';
301+
const cookies2 = ['__Secure-has-sid=1', `sid=${sid}`, `oid=${orgIdMinus3}`].join('; ').trim();
302+
303+
// Request the location header to establish our session with the servlet
304+
response = await axios.get(urlObj.toString(), {
305+
headers: {
306+
Cookie: cookies2,
307+
},
308+
withCredentials: true,
309+
maxRedirects: 0, // Prevent axios from following redirects
310+
validateStatus: (status) => status >= 200 && status < 400, // Accept 3xx status codes
311+
});
312+
const setCookieHeader = response.headers['set-cookie'];
313+
if (setCookieHeader) {
314+
// Find the 'sid' cookie in the set-cookie header
315+
const sidCookie = setCookieHeader.find((cookieStr: string) => cookieStr.startsWith('sid='));
316+
if (sidCookie) {
317+
// Extract the sid value from the set-cookie string
318+
const sidMatch = sidCookie.match(/sid=([^;]+)/);
319+
if (sidMatch?.[1]) {
320+
const sidToken = sidMatch[1];
321+
return sidToken;
322+
}
323+
}
324+
}
325+
}
326+
327+
// if we can't establish a valid session this way, lets just warn the user and utilize the guest user context for the site
328+
// eslint-disable-next-line no-console
329+
console.warn(
330+
`Warning: could not establish valid auth token for your site '${this.siteDisplayName}'.` +
331+
'Local Dev proxied requests to your site may fail or return data from the guest user context.'
332+
);
333+
334+
return ''; // Site will be guest user access only
335+
}
336+
337+
// Not sure what scenarios we don't have an access token at all, but lets output a separate message here so we can distinguish these edge cases
338+
// eslint-disable-next-line no-console
339+
console.warn(
340+
'Warning: sf cli org connection missing accessToken. Local Dev proxied requests to your site may fail or return data from the guest user context.'
341+
);
342+
return '';
343+
}
228344
}
229345

230346
/**

yarn.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6126,6 +6126,15 @@ axe-core@^4.6.2:
61266126
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59"
61276127
integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==
61286128

6129+
axios@^1.7.7:
6130+
version "1.7.7"
6131+
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
6132+
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
6133+
dependencies:
6134+
follow-redirects "^1.15.6"
6135+
form-data "^4.0.0"
6136+
proxy-from-env "^1.1.0"
6137+
61296138
axobject-query@^3.1.1:
61306139
version "3.2.4"
61316140
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.4.tgz#6dfba930294ea14d7d2fc68b9d007211baedb94c"
@@ -8695,6 +8704,11 @@ follow-redirects@^1.0.0:
86958704
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
86968705
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
86978706

8707+
follow-redirects@^1.15.6:
8708+
version "1.15.9"
8709+
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
8710+
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
8711+
86988712
for-each@^0.3.3:
86998713
version "0.3.3"
87008714
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -12991,6 +13005,11 @@ proxy-addr@~2.0.7:
1299113005
forwarded "0.2.0"
1299213006
ipaddr.js "1.9.1"
1299313007

13008+
proxy-from-env@^1.1.0:
13009+
version "1.1.0"
13010+
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
13011+
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
13012+
1299413013
psl@^1.1.33:
1299513014
version "1.9.0"
1299613015
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"

0 commit comments

Comments
 (0)