Skip to content

Commit

Permalink
Merge pull request #303 from adobe/issue-298
Browse files Browse the repository at this point in the history
feat: add auth token to every admin api request
  • Loading branch information
rofe committed Apr 28, 2023
2 parents 4f04585 + 2dfe0cb commit 5b9e837
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 169 deletions.
93 changes: 90 additions & 3 deletions src/extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
getGitHubSettings,
setConfig,
getConfig,
storeAuthToken,
updateProjectConfigs,
} from './utils.js';

Expand Down Expand Up @@ -117,8 +116,10 @@ async function guessIfFranklinSite({ id }) {
});
// listen for response message from tab
const listener = ({ isFranklinSite }) => {
chrome.runtime.onMessage.removeListener(listener);
resolve(isFranklinSite);
if (typeof isFranklinSite === 'boolean') {
chrome.runtime.onMessage.removeListener(listener);
resolve(isFranklinSite);
}
};
chrome.runtime.onMessage.addListener(listener);
});
Expand Down Expand Up @@ -330,6 +331,75 @@ function checkViewDocSource(id) {
});
}

/**
* Sets the x-auth-token header for all requests to admin.hlx.page if project config
* has an auth token.
*/
async function updateAdminAuthHeaderRules() {
try {
// remove all rules first
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules())
.map((rule) => rule.id),
});
// find projects with auth tokens and add rules for each
let id = 1;
const projects = await getConfig('sync', 'hlxSidekickProjects') || [];
const addRules = [];
const projectConfigs = await Promise.all(projects.map((handle) => getConfig('sync', handle)));
projectConfigs.forEach(({ owner, repo, authToken }) => {
if (authToken) {
addRules.push({
id,
priority: 1,
action: {
type: 'modifyHeaders',
requestHeaders: [{
operation: 'set',
header: 'x-auth-token',
value: authToken,
}],
},
condition: {
regexFilter: `^https://admin.hlx.page/[a-z]+/${owner}/${repo}/.*`,
requestDomains: ['admin.hlx.page'],
requestMethods: ['get', 'post', 'delete'],
resourceTypes: ['xmlhttprequest'],
},
});
id += 1;
log.debug('added admin auth header rule for ', owner, repo);
}
});
if (addRules.length > 0) {
await chrome.declarativeNetRequest.updateSessionRules({
addRules,
});
log.debug(`setAdminAuthHeaderRule: ${addRules.length} rule(s) set`);
}
} catch (e) {
log.error(`updateAdminAuthHeaderRules: ${e.message}`);
}
}

async function storeAuthToken(owner, repo, token) {
// find config tab with owner/repo
const project = await getProject({ owner, repo });
if (project) {
if (token) {
project.authToken = token;
} else {
delete project.authToken;
}
await setProject(project);
log.debug(`updated auth token for ${owner}--${repo}`);
} else {
log.debug(`unable to update auth token for ${owner}--${repo}: no such config`);
}
// auth token changed, set/update admin auth header
updateAdminAuthHeaderRules();
}

/**
* Adds the listeners for the extension.
*/
Expand All @@ -338,6 +408,7 @@ function checkViewDocSource(id) {
log.info(`sidekick extension installed (${reason})`);
await updateHelpContent();
await updateProjectConfigs();
await updateAdminAuthHeaderRules();
});

// register message listener
Expand Down Expand Up @@ -431,6 +502,22 @@ function checkViewDocSource(id) {
actions[actionFromTab](tab);
}
});

// listen for delete auth token calls from the content window
chrome.runtime.onMessage.addListener(async ({ deleteAuthToken }, { tab }) => {
// check if message contains project config and is sent from tab
if (tab && tab.id && typeof deleteAuthToken === 'object') {
const { owner, repo } = deleteAuthToken;
await storeAuthToken(owner, repo, '');
}
});

// for local debugging of header modification rules:
// 1. add "declarativeNetRequestFeedback" to permissions in manifest.json
// 2. uncomment the following 3 lines:
// chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(({ request, rule }) => {
// console.log('rule matched', request.method, request.url, rule.ruleId);
// });
})();

// announce sidekick display state
Expand Down
6 changes: 4 additions & 2 deletions src/extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
"options_page": "options.html",
"description": "__MSG_description__",
"permissions": [
"activeTab",
"contextMenus",
"declarativeNetRequest",
"scripting",
"storage",
"activeTab"
"storage"
],
"host_permissions": [
"https://www.hlx.live/tools/sidekick/*",
"http://localhost:3000/*",
"https://*/*"
],
Expand Down
158 changes: 84 additions & 74 deletions src/extension/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -888,15 +888,12 @@
* @param {object} config
* @returns {object}
*/
function getAdminFetchOptions({ authToken }) {
function getAdminFetchOptions() {
const opts = {
cache: 'no-store',
credentials: 'include',
headers: {},
};
if (authToken) {
opts.headers['x-auth-token'] = authToken;
}
return opts;
}

Expand Down Expand Up @@ -1791,6 +1788,18 @@
}
}

async function checkProfileStatus(sk, status) {
const url = getAdminUrl(sk.config, 'profile');
const opts = getAdminFetchOptions(sk.config);
return fetch(url, opts)
.then((res) => res.json())
.then((json) => {
console.log(json);
return json.status === status;
})
.catch(() => false);
}

/**
* Logs the user in.
* @private
Expand All @@ -1799,70 +1808,50 @@
*/
function login(sk, selectAccount) {
sk.showWait();
const loginUrl = getAdminUrl(
sk.config,
'login',
sk.isProject() ? sk.location.pathname : '',
);
loginUrl.searchParams.set('loginRedirect', 'https://www.hlx.live/tools/sidekick/login-success');
const loginUrl = getAdminUrl(sk.config, 'login');
const extensionId = window.chrome?.runtime?.id;
if (extensionId) {
const authHeaderEnabled = extensionId && !window.navigator.vendor.includes('Apple');
if (authHeaderEnabled) {
loginUrl.searchParams.set('extensionId', extensionId);
} else {
loginUrl.searchParams.set(
'loginRedirect',
'https://www.hlx.live/tools/sidekick/login-success',
);
}
if (selectAccount) {
loginUrl.searchParams.set('selectAccount', true);
}
const profileUrl = new URL('https://admin.hlx.page/profile');
if (sk.config.adminVersion) {
profileUrl.searchParams.append('hlx-admin-version', sk.config.adminVersion);
}
const loginWindow = window.open(loginUrl.toString());

async function checkLoggedIn() {
if ((await fetch(profileUrl.href, getAdminFetchOptions(sk.config))).ok) {
window.setTimeout(() => {
if (!loginWindow.closed) {
loginWindow.close();
}
}, 500);
delete sk.status.status;
sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true });
sk.fetchStatus();
fireEvent(sk, 'loggedin');
return true;
}
return false;
}
let attempts = 0;

let seconds = 0;
const loginCheck = window.setInterval(async () => {
// give up after 2 minutes or window closed
if (seconds >= 120 || loginWindow.closed) {
window.clearInterval(loginCheck);
loginWindow.close();
// last check
if (await checkLoggedIn()) {
async function checkLoggedIn() {
if (loginWindow.closed) {
attempts += 1;
// try 5 times after login window has been closed
if (await checkProfileStatus(sk, 200)) {
// logged in, stop checking
delete sk.status.status;
sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true });
sk.fetchStatus();
fireEvent(sk, 'loggedin');
return;
}

if (seconds >= 120) {
if (attempts >= 5) {
// give up after 5 attempts
sk.showModal({
message: i18n(sk, 'error_login_timeout'),
sticky: true,
level: 1,
});
} else {
sk.showModal({
messsage: i18n(sk, 'error_login_aborted'),
});
return;
}
}

seconds += 1;
if (await checkLoggedIn()) {
window.clearInterval(loginCheck);
}
}, 1000);
// try again after 1s
window.setTimeout(checkLoggedIn, 1000);
}
window.setTimeout(checkLoggedIn, 1000);
}

/**
Expand All @@ -1871,29 +1860,47 @@
* @param {Sidekick} sk The sidekick
*/
function logout(sk) {
const logoutUrl = new URL('https://admin.hlx.page/logout');
if (sk.config.adminVersion) {
logoutUrl.searchParams.append('hlx-admin-version', sk.config.adminVersion);
}

fetch(logoutUrl.href, {
...getAdminFetchOptions(sk.config),
})
.then(() => {
delete sk.config.authToken;
sk.status = {
loggedOut: true,
};
})
.then(() => fireEvent(sk, 'loggedout'))
.then(() => sk.fetchStatus())
.catch((e) => {
console.error('logout failed', e);
sk.showModal({
message: i18n(sk, 'error_logout_error'),
level: 0,
});
});
sk.showWait();
const logoutUrl = getAdminUrl(sk.config, 'logout');
const extensionId = window.chrome?.runtime?.id;
if (extensionId && !window.navigator.vendor.includes('Apple')) { // exclude safari
logoutUrl.searchParams.set('extensionId', extensionId);
} else {
logoutUrl.searchParams.set(
'logoutRedirect',
'https://www.hlx.live/tools/sidekick/logout-success',
);
}
const logoutWindow = window.open(logoutUrl.toString());

let attempts = 0;

async function checkLoggedOut() {
if (logoutWindow.closed) {
attempts += 1;
// try 5 times after login window has been closed
if (await checkProfileStatus(sk, 401)) {
delete sk.status.profile;
delete sk.config.authToken;
sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true });
sk.fetchStatus();
fireEvent(sk, 'loggedout');
return;
}
if (attempts >= 5) {
// give up after 5 attempts
sk.showModal({
message: i18n(sk, 'error_logout_error'),
sticky: true,
level: 1,
});
return;
}
}
// try again after 1s
window.setTimeout(checkLoggedOut, 1000);
}
window.setTimeout(checkLoggedOut, 1000);
}

/**
Expand Down Expand Up @@ -2643,7 +2650,10 @@
throw new Error('error_status_invalid');
}
})
.then((json) => Object.assign(this.status, json))
.then((json) => {
this.status = json;
return json;
})
.then((json) => fireEvent(this, 'statusfetched', json))
.catch(({ message }) => {
this.status.error = message;
Expand Down
6 changes: 0 additions & 6 deletions src/extension/sidekick.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
setConfig,
setDisplay,
i18n,
storeAuthToken,
} from './utils.js';

export default async function injectSidekick(config, display) {
Expand Down Expand Up @@ -105,11 +104,6 @@ export default async function injectSidekick(config, display) {
sk.addEventListener('hidden', () => {
setDisplay(false);
});
sk.addEventListener('loggedout', async () => {
// user clicked logout, delete the authToken from the config
log.debug(`removing authToken from config ${owner}/${repo}`);
await storeAuthToken(owner, repo, '');
});
const helpOptOut = await getConfig('sync', 'hlxSidekickHelpOptOut');
if (!helpOptOut) {
// find next unacknowledged help topic with matching condition
Expand Down
Loading

0 comments on commit 5b9e837

Please sign in to comment.