Skip to content

Commit

Permalink
feat: establish valid experience site sessions at startup (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
nrkruk authored Sep 20, 2024
1 parent 809b895 commit 1f29721
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@salesforce/sf-plugins-core": "^11.2.4",
"@inquirer/select": "^2.4.7",
"@inquirer/prompts": "^5.3.8",
"axios": "^1.7.7",
"chalk": "^5.3.0",
"lwc": "7.1.3",
"lwr": "0.14.3",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/lightning/dev/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export default class LightningDevSite extends SfCommand<void> {
}
}

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

// Start the dev server
await expDev({
Expand Down
116 changes: 116 additions & 0 deletions src/shared/experience/expSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { Org, SfError } from '@salesforce/core';
import axios from 'axios';

export type SiteMetadata = {
bundleName: string;
Expand Down Expand Up @@ -94,6 +95,28 @@ export class ExperienceSite {
return experienceSites;
}

/**
* Esablish a valid token for this local development session
*
* @returns sid token for proxied site requests
*/
public async setupAuth(): Promise<string> {
let sidToken = ''; // Default to guest user access only

// Use environment variable for now if users want to just have guest access only
if (process.env.SITE_GUEST_ACCESS !== 'true') {
try {
const networkId = await this.getNetworkId();
sidToken = await this.getNewSidToken(networkId);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to establish authentication for site', e);
}
}

return sidToken;
}

public async isUpdateAvailable(): Promise<boolean> {
const localMetadata = this.getLocalMetadata();
if (!localMetadata) {
Expand Down Expand Up @@ -225,6 +248,99 @@ export class ExperienceSite {

return resourcePath;
}

private async getNetworkId(): Promise<string> {
const conn = this.org.getConnection();
// Query the Network object for the network with the given site name
const result = await conn.query<{ Id: string }>(`SELECT Id FROM Network WHERE Name = '${this.siteDisplayName}'`);

const record = result.records[0];
if (record) {
let networkId = record.Id;
// Subtract the last three characters from the Network ID
networkId = networkId.substring(0, networkId.length - 3);
return networkId;
} else {
throw new Error(`NetworkId for site: '${this.siteDisplayName}' could not be found`);
}
}

private async getNewSidToken(networkId: string): Promise<string> {
// Get the connection and access token from the org
const conn = this.org.getConnection();
const orgId = this.org.getOrgId();

// Not sure if we need to do this
const orgIdMinus3 = orgId.substring(0, orgId.length - 3);
const accessToken = conn.accessToken;
const instanceUrl = conn.instanceUrl; // Org URL

// Make the GET request without following redirects
if (accessToken) {
// TODO should we try and refresh auth here?
// await conn.refreshAuth();

// Call out to the switcher servlet to establish a session
const switchUrl = `${instanceUrl}/servlet/networks/switch?networkId=${networkId}`;
const cookies = [`sid=${accessToken}`, `oid=${orgIdMinus3}`].join('; ').trim();
let response = await axios.get(switchUrl, {
headers: {
Cookie: cookies,
},
withCredentials: true,
maxRedirects: 0, // Prevent axios from following redirects
validateStatus: (status) => status >= 200 && status < 400, // Accept 3xx status codes
});

// Extract the Location callback header
const locationHeader = response.headers['location'] as string;
if (locationHeader) {
// Parse the URL to extract the 'sid' parameter
const urlObj = new URL(locationHeader);
const sid = urlObj.searchParams.get('sid') ?? '';
const cookies2 = ['__Secure-has-sid=1', `sid=${sid}`, `oid=${orgIdMinus3}`].join('; ').trim();

// Request the location header to establish our session with the servlet
response = await axios.get(urlObj.toString(), {
headers: {
Cookie: cookies2,
},
withCredentials: true,
maxRedirects: 0, // Prevent axios from following redirects
validateStatus: (status) => status >= 200 && status < 400, // Accept 3xx status codes
});
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader) {
// Find the 'sid' cookie in the set-cookie header
const sidCookie = setCookieHeader.find((cookieStr: string) => cookieStr.startsWith('sid='));
if (sidCookie) {
// Extract the sid value from the set-cookie string
const sidMatch = sidCookie.match(/sid=([^;]+)/);
if (sidMatch?.[1]) {
const sidToken = sidMatch[1];
return sidToken;
}
}
}
}

// if we can't establish a valid session this way, lets just warn the user and utilize the guest user context for the site
// eslint-disable-next-line no-console
console.warn(
`Warning: could not establish valid auth token for your site '${this.siteDisplayName}'.` +
'Local Dev proxied requests to your site may fail or return data from the guest user context.'
);

return ''; // Site will be guest user access only
}

// 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
// eslint-disable-next-line no-console
console.warn(
'Warning: sf cli org connection missing accessToken. Local Dev proxied requests to your site may fail or return data from the guest user context.'
);
return '';
}
}

/**
Expand Down
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6126,6 +6126,15 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59"
integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==

axios@^1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"

axobject-query@^3.1.1:
version "3.2.4"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.4.tgz#6dfba930294ea14d7d2fc68b9d007211baedb94c"
Expand Down Expand Up @@ -8695,6 +8704,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==

follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==

for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
Expand Down Expand Up @@ -12991,6 +13005,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"

proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==

psl@^1.1.33:
version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
Expand Down

0 comments on commit 1f29721

Please sign in to comment.