token usage tightened with revocation

This commit is contained in:
2026-08-26 18:17:55 +02:00
parent 1a24d21d0a
commit 4049a197b9
13 changed files with 288 additions and 54 deletions
+4 -1
View File
@@ -12,6 +12,9 @@
"http://*/*",
"https://*/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'"
},
"action": {
"default_title": "__MSG_extensionName__",
"default_popup": "popup.html",
@@ -27,7 +30,7 @@
"browser_specific_settings": {
"gecko": {
"id": "linklog@kolkman.org",
"strict_min_version": "109.0",
"strict_min_version": "112.0",
"data_collection_permissions": {
"required": ["websiteActivity"],
"optional": []
+54 -9
View File
@@ -10,6 +10,7 @@ const otpInput = document.getElementById('otp');
const session = document.getElementById('logged-in');
const sessionSummary = document.getElementById('session-summary');
const signOutButton = document.getElementById('sign-out');
const sessionStore = browser.storage.session;
const t = window.linklogI18n;
@@ -49,15 +50,18 @@ function setStatus(message, isError = false) {
}
async function loadSettings() {
const settings = await browser.storage.local.get(['backendUrl', 'email', 'username', 'accessToken']);
const [settings, sessionSettings] = await Promise.all([
browser.storage.local.get(['backendUrl', 'email', 'username']),
sessionStore.get(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']),
]);
backendUrlInput.value = settings.backendUrl || '';
emailInput.value = settings.email || '';
if (settings.accessToken && settings.backendUrl && await hasBackendPermission(settings.backendUrl)) {
if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(settings.backendUrl)) {
try {
const response = await fetch(
`${settings.backendUrl}/api/auth/me`,
{headers: {Authorization: `Bearer ${settings.accessToken}`}},
{headers: {Authorization: `Bearer ${sessionSettings.accessToken}`}},
);
if (response.ok) {
const user = await response.json();
@@ -84,7 +88,32 @@ function showLoggedOut() {
}
async function clearSession() {
await browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken']);
await Promise.all([
sessionStore.remove(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']),
browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken', 'deviceId']),
]);
}
async function refreshAccessToken(settings) {
if (!settings.backendUrl || !settings.refreshToken || !settings.deviceId) return null;
try {
const response = await fetch(`${settings.backendUrl}/api/auth/refresh`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({refresh_token: settings.refreshToken, device_id: settings.deviceId}),
});
if (!response.ok) {
await clearSession();
return null;
}
const data = await response.json();
const refreshed = {accessToken: data.access_token, refreshToken: data.refresh_token,
tokenExpiresAt: data.expires_at, deviceId: data.device_id || settings.deviceId};
await sessionStore.set(refreshed);
return refreshed;
} catch (error) {
return null;
}
}
async function saveSettingsAndLogin(event) {
@@ -116,14 +145,29 @@ async function saveSettingsAndLogin(event) {
}
const data = await response.json();
if (response.status === 401) {
const refreshed = await refreshAccessToken({...settings, ...sessionSettings});
if (refreshed) {
const retry = await fetch(`${settings.backendUrl}/api/auth/me`, {
headers: {Authorization: `Bearer ${refreshed.accessToken}`},
});
if (retry.ok) {
const user = await retry.json();
showLoggedIn(user.username || settings.email, settings.backendUrl);
return;
}
}
}
await browser.storage.local.set({
backendUrl,
email,
username: data.user?.username || email,
});
await sessionStore.set({
accessToken: data.access_token,
tokenType: data.token_type,
tokenExpiresAt: data.expires_at,
refreshToken: data.refresh_token,
tokenExpiresAt: data.expires_at,
deviceId: data.device_id || `device-${crypto.randomUUID()}`,
});
showLoggedIn(data.user?.username || email, backendUrl);
@@ -136,13 +180,14 @@ async function saveSettingsAndLogin(event) {
}
async function signOut() {
const settings = await browser.storage.local.get(['accessToken']);
const settings = await browser.storage.local.get(['backendUrl']);
const sessionSettings = await sessionStore.get(['accessToken']);
const backendUrl = normalizeBackendOrigin(backendUrlInput.value.trim());
if (settings.accessToken && await hasBackendPermission(backendUrl)) {
if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(backendUrl)) {
await fetch(`${backendUrl}/api/auth/logout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: settings.accessToken }),
body: JSON.stringify({ token: sessionSettings.accessToken }),
}).catch(() => undefined);
}
await clearSession();
+63 -13
View File
@@ -13,6 +13,7 @@ const feedLink = document.getElementById('feed-link');
const authWarning = document.getElementById('auth-warning');
const warningSettingsButton = document.getElementById('warning-settings');
const authSession = document.getElementById('auth-session');
const sessionStore = browser.storage.session;
const t = window.linklogI18n;
@@ -42,12 +43,49 @@ function setStatus(message, isError = false) {
}
async function getSettings() {
const result = await browser.storage.local.get([
'backendUrl',
'accessToken',
'tokenExpiresAt',
const [settings, sessionSettings] = await Promise.all([
browser.storage.local.get(['backendUrl', 'username']),
sessionStore.get(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']),
]);
return result;
return {...settings, ...sessionSettings};
}
async function persistSession(settings) {
await sessionStore.set({
accessToken: settings.accessToken,
refreshToken: settings.refreshToken,
tokenExpiresAt: settings.tokenExpiresAt,
deviceId: settings.deviceId,
});
}
async function clearSession() {
await Promise.all([
sessionStore.remove(['accessToken', 'refreshToken', 'tokenExpiresAt', 'deviceId']),
browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken', 'deviceId']),
]);
}
async function refreshAccessToken(settings) {
if (!settings.backendUrl || !settings.refreshToken || !settings.deviceId) return null;
try {
const response = await fetch(`${settings.backendUrl}/api/auth/refresh`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({refresh_token: settings.refreshToken, device_id: settings.deviceId}),
});
if (!response.ok) {
await clearSession();
return null;
}
const data = await response.json();
const refreshed = {...settings, accessToken: data.access_token, refreshToken: data.refresh_token,
tokenExpiresAt: data.expires_at, deviceId: data.device_id || settings.deviceId};
await persistSession(refreshed);
return refreshed;
} catch (error) {
return null;
}
}
async function validateSession(settings) {
@@ -57,7 +95,16 @@ async function validateSession(settings) {
headers: {'Authorization': `Bearer ${settings.accessToken}`},
});
if (!response.ok) {
await browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken']);
if (response.status === 401) {
const refreshed = await refreshAccessToken(settings);
if (refreshed) {
const retry = await fetch(`${refreshed.backendUrl}/api/auth/me`, {
headers: {'Authorization': `Bearer ${refreshed.accessToken}`},
});
if (retry.ok) return await retry.json();
}
}
await clearSession();
return null;
}
return await response.json();
@@ -97,7 +144,7 @@ function showSavedState(message) {
}
async function updateFeedLink() {
const settings = await browser.storage.local.get(['backendUrl', 'username', 'accessToken']);
const settings = await getSettings();
if (!settings.backendUrl || !settings.username || !settings.accessToken) return;
try {
const backend = new URL(settings.backendUrl);
@@ -119,9 +166,10 @@ async function loadExistingTags() {
showSignedOutState();
return;
}
showSignedInState(user, settings.backendUrl);
const response = await fetch(`${settings.backendUrl}/api/tags`, {
headers: {'Authorization': `Bearer ${settings.accessToken}`},
const currentSettings = await getSettings();
showSignedInState(user, currentSettings.backendUrl);
const response = await fetch(`${currentSettings.backendUrl}/api/tags`, {
headers: {'Authorization': `Bearer ${currentSettings.accessToken}`},
});
if (!response.ok) {
if (response.status === 401) {
@@ -212,12 +260,14 @@ async function handleSubmit(event) {
return;
}
const currentSettings = await getSettings();
try {
const response = await fetch(`${backendUrl}/api/links`, {
const response = await fetch(`${currentSettings.backendUrl}/api/links`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'Authorization': `Bearer ${currentSettings.accessToken}`,
},
body: JSON.stringify({
title: titleInput.value,
@@ -240,7 +290,7 @@ async function handleSubmit(event) {
}
const result = await response.json();
const saveMessage = result.duplicate ? t('linkAlreadyExists') : t('linkSaved', backendUrl);
const saveMessage = result.duplicate ? t('linkAlreadyExists') : t('linkSaved', currentSettings.backendUrl);
if (result.plugin_errors?.length) {
const errors = result.plugin_errors.map((error) => `${error.plugin}: ${error.reason}`).join(' ');
showSavedState(`${saveMessage} ${t('publishingErrors', errors)}`);