diff --git a/public/js/auth.js b/public/js/auth.js index 9e2b73d..260b52a 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -16,6 +16,19 @@ document.addEventListener('DOMContentLoaded', function() { var USER_KEY = 'ped_scribe_user'; var SESSION_KEY = 'ped_session_id'; + // Runtime-split auth model: + // - Web browser → httpOnly cookie only (no localStorage token, XSS-safe). + // fetch() defaults to credentials:'same-origin' so cookies are sent. + // - Capacitor native app → Bearer token in iOS Keychain / Android Keystore + // via SecureStorage wrapper. WebView cookies can be evicted; Keychain + // survives cold starts reliably. + function isNativeApp() { + try { + return !!(window.Capacitor && typeof window.Capacitor.isNativePlatform === 'function' && window.Capacitor.isNativePlatform()); + } catch(e) { return false; } + } + window.IS_NATIVE_APP = isNativeApp(); + // Auth module initialized // Make sure forms are visible/hidden correctly on load @@ -33,9 +46,8 @@ document.addEventListener('DOMContentLoaded', function() { if (ssoOk === 'ok') { var ssoSid = urlParams.get('sid'); history.replaceState(null, '', window.location.pathname); - if (ssoSid) { - if (window.SecureStorage) window.SecureStorage.set(SESSION_KEY, ssoSid); - else localStorage.setItem(SESSION_KEY, ssoSid); + if (ssoSid && isNativeApp()) { + window.SecureStorage.set(SESSION_KEY, ssoSid); } // Token is in httpOnly cookie — verify via /me endpoint (cookie sent automatically) fetch('/api/auth/me', { credentials: 'same-origin' }) @@ -75,43 +87,35 @@ document.addEventListener('DOMContentLoaded', function() { }) .catch(function() {}); - var hydrate = window.SecureStorage - ? window.SecureStorage.hydrate([TOKEN_KEY, USER_KEY, SESSION_KEY]) - : Promise.resolve(); - - hydrate.then(function() { - var savedToken = window.SecureStorage - ? window.SecureStorage.getSync(TOKEN_KEY) - : localStorage.getItem(TOKEN_KEY); - - if (ssoOk) { - // SSO handled above — do nothing here - } else if (ssoError) { - // SSO error handled above — auth screen already shown - } else if (savedToken) { - fetch('/api/auth/me', { - headers: { 'Authorization': 'Bearer ' + savedToken } - }) - .then(function(r) { - if (r.ok) return r.json(); - throw new Error('expired'); - }) - .then(function(data) { - if (data && data.user) { - enterApp(data.user, savedToken); - } else { - showAuthScreen(); - clearSession(); - } - }) - .catch(function() { - showAuthScreen(); - clearSession(); - }); + if (isNativeApp()) { + // Native app path: token lives in Keychain/Keystore via SecureStorage + window.SecureStorage.hydrate([TOKEN_KEY, USER_KEY, SESSION_KEY]).then(function() { + var savedToken = window.SecureStorage.getSync(TOKEN_KEY); + if (ssoOk || ssoError) return; // SSO branch handled above + if (!savedToken) { showAuthScreen(); return; } + fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + savedToken } }) + .then(function(r) { if (r.ok) return r.json(); throw new Error('expired'); }) + .then(function(data) { + if (data && data.user) enterApp(data.user, savedToken); + else { showAuthScreen(); clearSession(); } + }) + .catch(function() { showAuthScreen(); clearSession(); }); + }); + } else { + // Web path: rely on httpOnly cookie. fetch sends same-origin cookies + // automatically in modern browsers — no Authorization header needed. + if (ssoOk || ssoError) { + // SSO branch handled above } else { - showAuthScreen(); + fetch('/api/auth/me', { credentials: 'same-origin' }) + .then(function(r) { if (r.ok) return r.json(); throw new Error('not-logged-in'); }) + .then(function(data) { + if (data && data.user) enterApp(data.user, ''); + else showAuthScreen(); + }) + .catch(function() { showAuthScreen(); }); } - }); + } // ---- HELPER FUNCTIONS ---- @@ -135,13 +139,14 @@ document.addEventListener('DOMContentLoaded', function() { function enterApp(user, token) { window.AUTH_TOKEN = token; - if (window.SecureStorage) { + if (isNativeApp()) { + // Mobile: persist token to Keychain/Keystore window.SecureStorage.set(TOKEN_KEY, token); window.SecureStorage.set(USER_KEY, JSON.stringify(user)); - } else { - localStorage.setItem(TOKEN_KEY, token); - localStorage.setItem(USER_KEY, JSON.stringify(user)); } + // Web: do not persist the token anywhere — session lives in the + // httpOnly cookie already set by the server. User info is + // re-fetched via /api/auth/me on each boot. if (authScreen) authScreen.style.display = 'none'; if (mainApp) { @@ -206,14 +211,18 @@ document.addEventListener('DOMContentLoaded', function() { } function clearSession() { - if (window.SecureStorage) { + if (isNativeApp()) { window.SecureStorage.remove(TOKEN_KEY); window.SecureStorage.remove(USER_KEY); window.SecureStorage.remove(SESSION_KEY); } else { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(USER_KEY); - localStorage.removeItem(SESSION_KEY); + // Web: cookie cleared by /api/auth/logout. Also wipe any legacy + // localStorage entries from the dual-mode era so they don't linger. + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(USER_KEY); + localStorage.removeItem(SESSION_KEY); + } catch(e) {} } window.AUTH_TOKEN = null; // Clear service worker caches so a logged-out user on a shared device @@ -228,12 +237,17 @@ document.addEventListener('DOMContentLoaded', function() { } window.getAuthHeaders = function() { - var token = window.AUTH_TOKEN; - if (!token && window.SecureStorage) token = window.SecureStorage.getSync(TOKEN_KEY); - if (!token) { try { token = localStorage.getItem(TOKEN_KEY); } catch(e) {} } + // Web: no Authorization header. The httpOnly cookie is sent automatically + // by fetch (default credentials = same-origin). Server middleware falls + // back to cookie when Bearer is absent. + if (!isNativeApp()) { + return { 'Content-Type': 'application/json' }; + } + // Native: Bearer from Keychain/Keystore via SecureStorage. + var token = window.AUTH_TOKEN || window.SecureStorage.getSync(TOKEN_KEY) || ''; return { 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + (token || '') + 'Authorization': 'Bearer ' + token }; }; @@ -416,9 +430,8 @@ document.addEventListener('DOMContentLoaded', function() { } if (data.success && data.token && data.user) { - if (data.sessionId) { - if (window.SecureStorage) window.SecureStorage.set(SESSION_KEY, data.sessionId); - else localStorage.setItem(SESSION_KEY, data.sessionId); + if (data.sessionId && isNativeApp()) { + window.SecureStorage.set(SESSION_KEY, data.sessionId); } enterApp(data.user, data.token); showToast('Welcome, ' + data.user.name + '!', 'success'); @@ -509,9 +522,8 @@ document.addEventListener('DOMContentLoaded', function() { hideLoading(); if (data.success && data.token && data.user) { - if (data.sessionId) { - if (window.SecureStorage) window.SecureStorage.set(SESSION_KEY, data.sessionId); - else localStorage.setItem(SESSION_KEY, data.sessionId); + if (data.sessionId && isNativeApp()) { + window.SecureStorage.set(SESSION_KEY, data.sessionId); } enterApp(data.user, data.token); showToast(data.message || 'Account created!', 'success'); diff --git a/public/js/documents.js b/public/js/documents.js index 480e02e..940c22f 100644 --- a/public/js/documents.js +++ b/public/js/documents.js @@ -64,7 +64,10 @@ showLoading('Uploading document...'); fetch('/api/documents/upload', { method: 'POST', - headers: { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || (window.SecureStorage && window.SecureStorage.getSync('ped_scribe_token')) || localStorage.getItem('ped_scribe_token') || '') }, + headers: window.IS_NATIVE_APP + ? { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || (window.SecureStorage && window.SecureStorage.getSync('ped_scribe_token')) || '') } + : {}, // web: httpOnly cookie sent automatically via credentials:'same-origin' default + credentials: 'same-origin', body: formData }) .then(function(r) { return r.json(); })