Skip to content

WIP: feat: Enhanced Client Side Auth Injection #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/extension/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function showSidekickNotification(tabId, data, callback) {
* @param {string} origin
* @returns {boolean} true - trusted / false - untrusted
*/
function isGetAuthInfoTrustedOrigin(origin) {
export function isGetAuthInfoTrustedOrigin(origin) {
const TRUSTED_ORIGINS = [
ADMIN_ORIGIN,
'https://labs.aem.live',
Expand Down
39 changes: 32 additions & 7 deletions src/extension/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,41 @@
* all requests from tools.aem.live and labs.aem.live.
* @returns {Promise<void>}
*/
export async function configureAuthAndCorsHeaders() {
export async function configureAuthAndCorsHeaders(projectFilter) {
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
const projects = await getConfig('session', 'projects') || [];
const addRulesPromises = projects.map(async ({

if (!projectFilter || projectFilter.length === 0) {
return;

Check warning on line 40 in src/extension/auth.js

View check run for this annotation

Codecov / codecov/patch

src/extension/auth.js#L40

Added line #L40 was not covered by tests
}

const sessionProjects = await getConfig('session', 'projects') || [];

let result = [];
if (projectFilter !== 'all') {
projectFilter.forEach((project) => {
const sessionOrgProject = sessionProjects.find(
(sessionProject) => sessionProject.id === project.owner,
);
if (sessionOrgProject) {
result.push(sessionOrgProject);
}
const sessionSiteProject = sessionProjects.find(
(sessionProject) => sessionProject.id === `${project.owner}/${project.repo}`,
);
if (sessionSiteProject) {
result.push(sessionSiteProject);
}
});
} else {
result = sessionProjects;

Check warning on line 62 in src/extension/auth.js

View check run for this annotation

Codecov / codecov/patch

src/extension/auth.js#L62

Added line #L62 was not covered by tests
}

const addRulesPromises = result.map(async ({
owner, repo, authToken, siteToken,
}) => {
const rules = [];
Expand Down Expand Up @@ -165,7 +190,7 @@
const siteHandle = `${owner}/${repo}`;
const siteExists = projects.find(({ id }) => id === siteHandle);
if (authToken) {
authTokenExpiry *= 1000; // store in milliseconds
authTokenExpiry *= 1000;
if (!orgExists) {
projects.push({
id: orgHandle,
Expand All @@ -188,12 +213,13 @@
}
if (siteToken) {
if (!siteExists) {
siteTokenExpiry *= 1000; // store in milliseconds
projects.push({
id: siteHandle,
owner,
repo,
siteToken,
siteTokenExpiry: siteTokenExpiry * 1000, // store in milliseconds
siteTokenExpiry,
});
} else {
const siteIndex = projects.findIndex(({ id }) => id === siteHandle);
Expand All @@ -206,6 +232,5 @@
projects.splice(siteIndex, 1);
}
await setConfig('session', { projects });
await configureAuthAndCorsHeaders();
}
}
8 changes: 2 additions & 6 deletions src/extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
internalActions,
checkViewDocSource,
} from './actions.js';
import { configureAuthAndCorsHeaders } from './auth.js';

chrome.action.onClicked.addListener(async () => {
// toggle the sidekick when the action is clicked
Expand All @@ -35,8 +34,8 @@ chrome.tabs.onUpdated.addListener(async (id, info, tab) => {
}
});

chrome.tabs.onActivated.addListener(({ tabId: id }) => {
checkTab(id);
chrome.tabs.onActivated.addListener((tab) => {
checkTab(tab.tabId);
});

// external messaging API to execute actions
Expand Down Expand Up @@ -75,7 +74,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return false;
});

// add existing auth token headers
configureAuthAndCorsHeaders();

log.info('sidekick initialized');
8 changes: 8 additions & 0 deletions src/extension/tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import { urlCache } from './url-cache.js';
import { updateUI } from './ui.js';
import { getConfig } from './config.js';
import { configureAuthAndCorsHeaders } from './auth.js';
import { isGetAuthInfoTrustedOrigin } from './actions.js';

/**
* Loads the content script in the tab.
Expand Down Expand Up @@ -67,6 +69,12 @@

injectContentScript(id, matches, adminVersion);

if (!isGetAuthInfoTrustedOrigin(new URL(url).origin)) {
await configureAuthAndCorsHeaders(matches);
} else {
await configureAuthAndCorsHeaders('all');

Check warning on line 75 in src/extension/tab.js

View check run for this annotation

Codecov / codecov/patch

src/extension/tab.js#L75

Added line #L75 was not covered by tests
}

updateUI({
id, url, config, matches,
});
Expand Down
111 changes: 79 additions & 32 deletions test/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { expect } from '@open-wc/testing';
import { setUserAgent } from '@web/test-runner-commands';
import sinon from 'sinon';

import { configureAuthAndCorsHeaders, setAuthToken } from '../src/extension/auth.js';
import { setAuthToken } from '../src/extension/auth.js';
import chromeMock from './mocks/chrome.js';
import { error } from './test-utils.js';
import { checkTab } from '../src/extension/tab.js';

// @ts-ignore
window.chrome = chromeMock;
Expand All @@ -35,51 +35,76 @@ describe('Test auth', () => {
sandbox.restore();
});

it('configureAuthAndCorsHeaders', async () => {
const getSessionRules = sandbox.stub(chrome.declarativeNetRequest, 'getSessionRules')
// @ts-ignore
.resolves([{ id: 1 }]);
const updateSessionRules = sandbox.spy(chrome.declarativeNetRequest, 'updateSessionRules');
await configureAuthAndCorsHeaders();
expect(getSessionRules.called).to.be.true;
expect(updateSessionRules.called).to.be.true;
// error handling
updateSessionRules.restore();
sandbox.stub(chrome.declarativeNetRequest, 'updateSessionRules')
.throws(error);
await configureAuthAndCorsHeaders();
});

it('setAuthToken', async () => {
const updateSessionRules = sandbox.spy(chrome.declarativeNetRequest, 'updateSessionRules');
const getConfig = sandbox.spy(chrome.storage.session, 'get');
const setConfig = sandbox.spy(chrome.storage.session, 'set');
const owner = 'test';
const repo = 'site';
const authToken = '1234567890';
const authTokenExpiry = Date.now() / 1000 + 60;

const mockTab = { id: 1234, url: 'https://main--site--test.aem.live' };
sandbox.stub(chrome.tabs, 'query')
.withArgs({ active: true, currentWindow: true })
.resolves([mockTab]);

sandbox.stub(chrome.tabs, 'get')
.withArgs(mockTab.id)
.resolves(mockTab);

// set auth token
await setAuthToken(owner, repo, authToken, authTokenExpiry);
expect(getConfig.callCount).to.equal(2);
expect(setConfig.callCount).to.be.equal(1);
expect(setConfig.calledWith({
projects: [
{
id: owner,
owner,
repo,
authToken,
authTokenExpiry: authTokenExpiry * 1000,
picture: undefined,
},
],
})).to.be.true;

await checkTab(mockTab.id);

// update auth token without expiry
await setAuthToken(owner, repo, authToken);
expect(getConfig.callCount).to.equal(4);
expect(setConfig.callCount).to.be.equal(2);
expect(setConfig.calledWith({
projects: [
{
id: owner,
owner,
repo,
authToken,
authTokenExpiry: 0,
picture: undefined,
},
],
})).to.be.true;
await checkTab(mockTab.id);

// remove auth token
await setAuthToken(owner, repo, '');
expect(setConfig.callCount).to.equal(3);
expect(setConfig.calledWith({
projects: [],
})).to.be.true;
await checkTab(mockTab.id);

// remove auth token again
await setAuthToken(owner, repo, '');
expect(setConfig.callCount).to.equal(4);
expect(setConfig.calledWith({
projects: [],
})).to.be.true;
await checkTab(mockTab.id);

// testing else paths
getConfig.resetHistory();
setConfig.resetHistory();
// @ts-ignore
await setAuthToken();
expect(getConfig.notCalled).to.be.true;
expect(setConfig.notCalled).to.be.true;
await checkTab(mockTab.id);

expect(updateSessionRules.calledWith({
addRules: [
Expand Down Expand Up @@ -145,23 +170,36 @@ describe('Test auth', () => {

it('setAuthToken (added project)', async () => {
const updateSessionRules = sandbox.spy(chrome.declarativeNetRequest, 'updateSessionRules');
const getConfig = sandbox.spy(chrome.storage.session, 'get');
const setConfig = sandbox.spy(chrome.storage.session, 'set');
const owner = 'test';
const repo = 'site';
const authToken = '1234567890';

sandbox.stub(chrome.storage.sync, 'get').resolves({
projects: [
'test/site',
],
'test/site': {
owner,
repo,
host: 'production-host.com',
previewHost: 'custom-preview.com',
liveHost: 'custom-live.com',
},
});

const mockTab = { id: 1234, url: 'https://production-host.com' };
sandbox.stub(chrome.tabs, 'query')
.withArgs({ active: true, currentWindow: true })
.resolves([mockTab]);

sandbox.stub(chrome.tabs, 'get')
.withArgs(mockTab.id)
.resolves(mockTab);

await setAuthToken(owner, repo, authToken);
expect(setConfig.callCount).to.equal(1);
expect(getConfig.callCount).to.equal(2);
await checkTab(mockTab.id);

expect(updateSessionRules.calledWith({
addRules: [
Expand Down Expand Up @@ -308,17 +346,25 @@ describe('Test auth', () => {

it('setAuthToken with transient site token', async () => {
const updateSessionRules = sandbox.spy(chrome.declarativeNetRequest, 'updateSessionRules');
const getConfig = sandbox.spy(chrome.storage.session, 'get');
const setConfig = sandbox.spy(chrome.storage.session, 'set');
const owner = 'test';
const repo = 'site';
const authToken = '1234567890';
const siteToken = '0987654321';
let expiry = Date.now() / 1000 + 60;

const mockTab = { id: 1234, url: 'https://main--site--test.aem.live' };
sandbox.stub(chrome.tabs, 'query')
.withArgs({ active: true, currentWindow: true })
.resolves([mockTab]);

sandbox.stub(chrome.tabs, 'get')
.withArgs(mockTab.id)
.resolves(mockTab);

await setAuthToken(owner, repo, authToken, expiry, siteToken, expiry);
expect(setConfig.callCount).to.equal(1);
expect(getConfig.callCount).to.equal(2);
await checkTab(mockTab.id);

expect(updateSessionRules.calledWith({
addRules: [
Expand Down Expand Up @@ -414,14 +460,15 @@ describe('Test auth', () => {
// update existing auth and site tokens
expiry = Date.now() / 1000 + 120;
await setAuthToken(owner, repo, authToken, expiry, siteToken, expiry);
await checkTab(mockTab.id);
expect(setConfig.callCount).to.equal(2);
expect(getConfig.callCount).to.equal(4);
expect(updateSessionRules.callCount).to.equal(4);

// remove existing auth and site tokens
await setAuthToken(owner, repo, '', undefined, '', undefined);
await checkTab(mockTab.id);

expect(setConfig.callCount).to.equal(3);
expect(getConfig.callCount).to.equal(6);
expect(updateSessionRules.callCount).to.equal(5);
});
});