Skip to content

Commit

Permalink
Auto-renew tokens before they expire
Browse files Browse the repository at this point in the history
Also try to negotiate a 24-hour long token upon login to avoid having to re-authenticate too often (for example after computer has been turned off overnight)

Upgrade to latest version of polyfill

Improve username/password field search algorithm

Fix Firefox compatibility as the badge count was not visible
  • Loading branch information
Gerard Chanekon authored and mulbc committed Mar 8, 2023
1 parent 3d72bbe commit 773d01a
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 132 deletions.
156 changes: 131 additions & 25 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
/* eslint-disable no-console */
/* global chrome */

function storageGetterProvider(storageType) {
return function (key, defaultValue) {
return new Promise(function (resolve, reject) {
try {
chrome.storage[storageType].get([key], function (result) {
const value = result[key] || defaultValue || null;
resolve(value);
});
} catch (error) {
reject(error);
}
});
};
const idealTokenTTL = '24h';

var tokenRenewalIntervalId;

if (!chrome.browserAction) {
chrome.browserAction = chrome.action;
}

const storage = {
storageGetterProvider: (storageType) => {
return function (key, defaultValue) {
return new Promise(function (resolve, reject) {
try {
chrome.storage[storageType].get([key], function (result) {
const value = result[key] || defaultValue || null;
resolve(value);
});
} catch (error) {
reject(error);
}
});
};
},

local: {
get: storageGetterProvider('local'),
get: (key, defaultValue) =>
storage.storageGetterProvider('local')(key, defaultValue),
},
sync: {
get: storageGetterProvider('sync'),
get: (key, defaultValue) =>
storage.storageGetterProvider('sync')(key, defaultValue),
},
};

Expand All @@ -32,20 +42,24 @@ class Vault {
this.base = `${this.address}/v1`;
}

async request(method, endpoint) {
async request(method, endpoint, content = null) {
const res = await fetch(this.base + endpoint, {
method: method.toUpperCase(),
headers: {
'X-Vault-Token': this.token,
'Content-Type': 'application/json',
},
body: content != null ? JSON.stringify(content) : null,
});

if (!res.ok) throw new Error(`Error calling: ${method.toUpperCase()} ${this.base}${endpoint} -> HTTP ${res.status} - ${res.statusText}`);

const json = await res.json();
if (!res.ok)
throw new Error(
`Error calling: ${method.toUpperCase()} ${
this.base
}${endpoint} -> HTTP ${res.status} - ${res.statusText}`
);

return json;
return await res.json();
}

list(endpoint) {
Expand All @@ -55,6 +69,10 @@ class Vault {
get(endpoint) {
return this.request('GET', endpoint);
}

post(endpoint, content) {
return this.request('POST', endpoint, content);
}
}

function storePathComponents(storePath) {
Expand All @@ -64,11 +82,12 @@ function storePathComponents(storePath) {
}
const pathComponents = path.split('/');
const storeRoot = pathComponents[0];
const storeSubPath = pathComponents.length > 0 ? pathComponents.slice(1).join('/') : '';
const storeSubPath =
pathComponents.length > 0 ? pathComponents.slice(1).join('/') : '';

return {
root: storeRoot,
subPath: storeSubPath
subPath: storeSubPath,
};
}

Expand All @@ -85,7 +104,7 @@ async function autoFillSecrets(message, sender) {
const storePath = await storage.sync.get('storePath');
const storeComponents = storePathComponents(storePath);

if (!vaultAddress || !vaultAddress) return;
if (!vaultToken || !vaultAddress) return;

const url = new URL(sender.tab.url);
const hostname = clearHostname(url.hostname);
Expand All @@ -95,7 +114,9 @@ async function autoFillSecrets(message, sender) {
let loginCount = 0;

for (const secret of secretList) {
const secretKeys = await vault.list(`/${storeComponents.root}/metadata/${storeComponents.subPath}/${secret}`);
const secretKeys = await vault.list(
`/${storeComponents.root}/metadata/${storeComponents.subPath}/${secret}`
);
for (const key of secretKeys.data.keys) {
const pattern = new RegExp(key);
const patternMatches = pattern.test(hostname);
Expand All @@ -117,12 +138,97 @@ async function autoFillSecrets(message, sender) {
}
}
if (loginCount > 0) {
chrome.action.setBadgeText({ text: '*', tabId: sender.tab.id });
chrome.browserAction.setBadgeText({ text: '*', tabId: sender.tab.id });
}
}

async function renewToken(force = false) {
const vaultToken = await storage.local.get('vaultToken');
const vaultAddress = await storage.sync.get('vaultAddress');

if (vaultToken) {
try {
const vault = new Vault(vaultToken, vaultAddress);
const token = await vault.get('/auth/token/lookup-self');

console.log(
`${new Date().toLocaleString()} Token will expire in ${
token.data.ttl
} seconds`
);
if (token.data.ttl > 3600) {
refreshTokenListener(1800000);
} else {
refreshTokenListener((token.data.ttl / 2) * 1000);
}

if (force || token.data.ttl <= 60) {
console.log(`${new Date().toLocaleString()} Renewing Token...`);
const newToken = await vault.post('/auth/token/renew-self', {
increment: idealTokenTTL,
});
console.log(
`${new Date().toLocaleString()} Token renewed. It will expire in ${
newToken.auth.lease_duration
} seconds`
);
}

await chrome.browserAction.setBadgeBackgroundColor({ color: '#1c98ed' });
} catch (e) {
console.log(e);
await chrome.browserAction.setBadgeBackgroundColor({ color: '#FF0000' });
await chrome.browserAction.setBadgeText({ text: '!' });

refreshTokenListener();
}
}
}

function refreshTokenListener(interval = 30000) {
if (tokenRenewalIntervalId) {
clearInterval(tokenRenewalIntervalId);
}

tokenRenewalIntervalId = setInterval(async () => {
await renewToken();
}, interval);
}

async function newStateHandler(newState) {
console.log(`${new Date().toLocaleString()} ${newState}`);
if (newState === 'active') {
await renewToken(false);
}

if (newState === 'locked') {
await renewToken(true);
}
}

function setupIdleListener() {
if (!chrome.idle.onStateChanged.hasListener(newStateHandler)) {
chrome.idle.onStateChanged.addListener(newStateHandler);
}
}

chrome.runtime.onMessage.addListener(function (message, sender) {
if (message.type === 'auto_fill_secrets') {
autoFillSecrets(message, sender).catch(console.error);
setupIdleListener();
}

if (message.type === 'auto_renew_token') {
refreshTokenListener();
}
});

chrome.runtime.onInstalled.addListener(() => {
refreshTokenListener();
setupIdleListener();
});

chrome.runtime.onStartup.addListener(() => {
refreshTokenListener();
setupIdleListener();
});
4 changes: 2 additions & 2 deletions browser-polyfill.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion browser-polyfill.min.js.map

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion common.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
/* eslint-disable no-unused-vars */
/* global browser */
/* global browser chrome */

function storePathComponents(storePath) {
let path = 'secret/vaultPass';
Expand All @@ -17,3 +17,7 @@ function storePathComponents(storePath) {
subPath: storeSubPath,
};
}

if (!browser.browserAction) {
browser.browserAction = chrome.browserAction ?? chrome.action;
}
29 changes: 26 additions & 3 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ function findUsernameNodeIn(
'input[id="userid"]',
'input[id="login"]',
'input[id="email"]',
'textarea[id="username"]',
'textarea[id="userid"]',
'textarea[id="login"]',
'textarea[id="email"]',
'[type="email"]',
'[name="user_name"]',
'[name="user"]',
Expand All @@ -73,7 +77,7 @@ function findUsernameNodeIn(

let usernameNode = null;
for (const node of allUsernameNodes) {
if (checkVisibility ? node.offsetParent : true) {
if (checkVisibility ? isVisible(node) : true) {
usernameNode = node;
break;
}
Expand All @@ -100,11 +104,30 @@ function fillIn(node, value) {
node.blur();
}

function isVisible(node) {
if (!node.offsetParent) {
return false;
}

if (node.style.display === 'none') {
return false;
}

if (node.style.visibility === 'hidden') {
return false;
}

const computedStyle = window.getComputedStyle(node);
const visibility = computedStyle.getPropertyValue('visibility');

return visibility !== 'hidden';
}

function findPasswordInput() {
// eslint-disable-next-line quotes
const passwordNodes = document.querySelectorAll("input[type='password']");
for (const node of passwordNodes) {
if (node.offsetParent) return node;
if (isVisible(node)) return node;
}

return passwordNodes.length > 0 ? passwordNodes[0] : null;
Expand All @@ -121,7 +144,7 @@ function handleFillCredits(request) {
// Go completely crazy and wild guess any visible input field for the username if empty formNode
// https://stackoverflow.com/a/21696585
const usernameNode = formNode
? findUsernameNodeIn(formNode)
? findUsernameNodeIn(formNode, true)
: findUsernameNodeIn(document, true, request.isUserTriggered);
if (!usernameNode) return;

Expand Down
56 changes: 28 additions & 28 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
{
"manifest_version": 3,
"name": "VaultPass",
"description": "A Chrome extension to leverage Hashicorp Vault as Credential Storage for teams",
"version": "2.3.3",
"action": {
"default_icon": "icons/logo128.png",
"default_popup": "popup.html",
"default_title": "VaultPass"
},
"icons": {
"48": "icons/logo48.png",
"128": "icons/logo128.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["browser-polyfill.min.js", "content.js", "common.js"]
}
],
"background": {
"service_worker": "background.js"
},
"permissions": ["activeTab", "storage", "clipboardWrite"]
}
{
"manifest_version": 3,
"name": "VaultPass",
"description": "A Chrome extension to leverage Hashicorp Vault as Credential Storage for teams",
"version": "2.3.3",
"action": {
"default_icon": "icons/logo128.png",
"default_popup": "popup.html",
"default_title": "VaultPass"
},
"icons": {
"48": "icons/logo48.png",
"128": "icons/logo128.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["browser-polyfill.min.js", "content.js", "common.js"]
}
],
"background": {
"service_worker": "background.js"
},
"permissions": ["activeTab", "storage", "clipboardWrite", "idle"]
}
Loading

0 comments on commit 773d01a

Please sign in to comment.