From 7a70c933c998e003a87545df383c4ef51c2fd335 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 3 Dec 2025 15:40:59 -0600 Subject: [PATCH] Various fixes and cleanup, regressions from prior commits. --- frontend/auth/App.vue | 11 ++++---- frontend/auth/admin/AdminApp.vue | 21 ++++++++++---- frontend/int/reset/ResetApp.vue | 22 ++++++--------- frontend/src/components/RestrictedAuth.vue | 10 +++++-- frontend/src/stores/auth.js | 32 ++++++++++++++++------ frontend/src/utils/api.js | 2 ++ frontend/src/utils/awaitable-websocket.js | 15 ++-------- frontend/src/utils/passkey.js | 31 +++++++++++---------- passkey/fastapi/api.py | 6 +++- passkey/fastapi/ws.py | 4 +-- 10 files changed, 87 insertions(+), 67 deletions(-) diff --git a/frontend/auth/App.vue b/frontend/auth/App.vue index ee63824..bacb679 100644 --- a/frontend/auth/App.vue +++ b/frontend/auth/App.vue @@ -26,15 +26,14 @@ const showBackMessage = ref(false) let validationTimer = null let authIframe = null -async function tryLoadUserInfo() { +async function loadUserInfo() { try { - await store.loadUserInfo() + store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' }) authenticated.value = true loading.value = false startSessionValidation() return true - } catch (error) { - // User info load failed - apiJson will show iframe if needed + } catch (e) { return false } } @@ -74,7 +73,7 @@ function handleAuthMessage(event) { hideAuthIframe() loading.value = true loadingMessage.value = 'Loading user profile...' - tryLoadUserInfo() + loadUserInfo() break case 'auth-error': @@ -150,7 +149,7 @@ onMounted(async () => { if (store.settings?.rp_name) document.title = store.settings.rp_name // Try to load user info - const success = await tryLoadUserInfo() + const success = await loadUserInfo() if (!success) { // Need authentication - show login iframe diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index 094f4f9..df6035e 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -152,27 +152,36 @@ async function loadPermissions() { permissions.value = await apiJson('/auth/api/admin/permissions') } +async function loadUserInfo() { + try { + info.value = await apiJson('/auth/api/user-info', { method: 'POST' }) + authenticated.value = true + return true + } catch (e) { + error.value = e.message + return false + } +} + async function load() { loading.value = true loadingMessage.value = 'Loading...' error.value = null try { - const data = await apiJson('/auth/api/user-info', { method: 'POST' }) - info.value = data - authenticated.value = true + if (!await loadUserInfo()) return // Check if user has required permissions - if (data.authenticated && !(data.is_global_admin || data.is_org_admin)) { + if (info.value.authenticated && !(info.value.is_global_admin || info.value.is_org_admin)) { // User is authenticated but lacks required permissions - show forbidden view error.value = 'You do not have permission to access this area.' loading.value = false return } - if (data.authenticated && (data.is_global_admin || data.is_org_admin)) { + if (info.value.authenticated && (info.value.is_global_admin || info.value.is_org_admin)) { await Promise.all([loadOrgs(), loadPermissions()]) } - if (!data.is_global_admin && data.is_org_admin && orgs.value.length === 1) { + if (!info.value.is_global_admin && info.value.is_org_admin && orgs.value.length === 1) { if (!window.location.hash || window.location.hash === '#overview') { currentOrgId.value = orgs.value[0].uuid window.location.hash = `#org/${currentOrgId.value}` diff --git a/frontend/int/reset/ResetApp.vue b/frontend/int/reset/ResetApp.vue index 6ece266..ace2c8e 100644 --- a/frontend/int/reset/ResetApp.vue +++ b/frontend/int/reset/ResetApp.vue @@ -145,7 +145,7 @@ async function registerPasskey() { } try { - await setSessionCookie(result.session_token) + await setSessionCookie(result) } catch (error) { loading.value = false const message = error?.message || 'Failed to establish session' @@ -153,23 +153,23 @@ async function registerPasskey() { return } - showMessage('Passkey registered successfully!', 'success', 2000) - setTimeout(() => { - loading.value = false - redirectHome() - }, 800) + showMessage('Passkey registered successfully!', 'success', 800) + setTimeout(() => { loading.value = false; goHome() }, 800) } -async function setSessionCookie(sessionToken) { +async function setSessionCookie(result) { + if (!result?.session_token) { + throw new Error('Registration response missing session_token') + } return await apiJson('/auth/api/set-session', { method: 'POST', headers: { - Authorization: `Bearer ${sessionToken}` + Authorization: `Bearer ${result.session_token}` } }) } -function redirectHome() { +function goHome() { const target = uiBasePath.value || '/auth/' if (window.location.pathname !== target) { history.replaceState(null, '', target) @@ -177,10 +177,6 @@ function redirectHome() { window.location.reload() } -function goHome() { - redirectHome() -} - function extractTokenFromPath() { const segments = window.location.pathname.split('/').filter(Boolean) if (!segments.length) return '' diff --git a/frontend/src/components/RestrictedAuth.vue b/frontend/src/components/RestrictedAuth.vue index 9021b90..76d16a7 100644 --- a/frontend/src/components/RestrictedAuth.vue +++ b/frontend/src/components/RestrictedAuth.vue @@ -153,7 +153,7 @@ async function authenticateUser() { emit('auth-error', { message, cancelled }) return } - try { await setSessionCookie(result.session_token) } catch (error) { + try { await setSessionCookie(result) } catch (error) { loading.value = false const message = error?.message || 'Failed to establish session' showMessage(message, 'error', 4000) @@ -186,9 +186,13 @@ function openProfile() { if (profileWindow) profileWindow.focus() } -async function setSessionCookie(sessionToken) { +async function setSessionCookie(result) { + if (!result?.session_token) { + console.error('setSessionCookie called with missing session_token:', result) + throw new Error('Authentication response missing session_token') + } return await apiJson('/auth/api/set-session', { - method: 'POST', headers: { Authorization: `Bearer ${sessionToken}` } + method: 'POST', headers: { Authorization: `Bearer ${result.session_token}` } }) } diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 1c8b68c..1d28199 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -38,18 +38,21 @@ export const useAuthStore = defineStore('auth', { }, duration) } }, - async setSessionCookie(sessionToken) { - const result = await apiJson('/auth/api/set-session', { + async setSessionCookie(result) { + if (!result?.session_token) { + console.error('setSessionCookie called with missing session_token:', result) + throw new Error('Authentication response missing session_token') + } + return await apiJson('/auth/api/set-session', { method: 'POST', - headers: {'Authorization': `Bearer ${sessionToken}`}, + headers: {'Authorization': `Bearer ${result.session_token}`}, }) - return result }, async register() { this.isLoading = true try { const result = await register() - await this.setSessionCookie(result.session_token) + await this.setSessionCookie(result) await this.loadUserInfo() this.selectView() return result @@ -62,7 +65,7 @@ export const useAuthStore = defineStore('auth', { try { const result = await authenticate() - await this.setSessionCookie(result.session_token) + await this.setSessionCookie(result) await this.loadUserInfo() this.selectView() @@ -83,7 +86,12 @@ export const useAuthStore = defineStore('auth', { this.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' }) console.log('User info loaded:', this.userInfo) } catch (error) { - this.showMessage(error.message || 'Failed to load user info', 'error', 5000) + // Suppress toast for 401/403 errors - the auth iframe will handle these + if (error.status === 401 || error.status === 403) { + console.log('Authentication required:', error.message) + } else { + this.showMessage(error.message || 'Failed to load user info', 'error', 5000) + } throw error } }, @@ -113,7 +121,10 @@ export const useAuthStore = defineStore('auth', { location.reload() } catch (error) { console.error('Logout error:', error) - this.showMessage(error.message, 'error') + // Suppress toast for 401/403 errors - the auth iframe will handle these + if (error.status !== 401 && error.status !== 403) { + this.showMessage(error.message, 'error') + } } }, async logoutEverywhere() { @@ -123,7 +134,10 @@ export const useAuthStore = defineStore('auth', { location.reload() } catch (error) { console.error('Logout-all error:', error) - this.showMessage(error.message, 'error') + // Suppress toast for 401/403 errors - the auth iframe will handle these + if (error.status !== 401 && error.status !== 403) { + this.showMessage(error.message, 'error') + } } }, } diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 71e4319..f3b53f8 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -315,6 +315,8 @@ export function shouldShowErrorToast(error) { // Don't show toast for user cancellations if (error instanceof AuthCancelledError) return false if (error.name === 'AbortError') return false + // Don't show toast for 401/403 errors - the auth iframe will handle these + if (error instanceof ApiError && (error.status === 401 || error.status === 403)) return false return true } diff --git a/frontend/src/utils/awaitable-websocket.js b/frontend/src/utils/awaitable-websocket.js index 1b02544..f68b734 100644 --- a/frontend/src/utils/awaitable-websocket.js +++ b/frontend/src/utils/awaitable-websocket.js @@ -5,8 +5,8 @@ class AwaitableWebSocket extends WebSocket { #opened = false constructor(resolve, reject, url, protocols, binaryType) { - // Support relative URLs even on old browsers that don't - super(new URL(url, location.href.replace(/^http/, 'ws')), protocols) + // Support relative URLs even on old browsers that don't natively support them + super(new URL(url, document.baseURI.replace(/^http/, 'ws')), protocols) this.binaryType = binaryType || 'blob' this.onopen = () => { this.#opened = true @@ -51,21 +51,12 @@ class AwaitableWebSocket extends WebSocket { console.error("WebSocket received binary data, expected JSON string", data) throw new Error("WebSocket received binary data, expected JSON string") } - let parsed try { - parsed = JSON.parse(data) + return JSON.parse(data) } catch (err) { console.error("Failed to parse JSON from WebSocket message", data, err) throw new Error("Failed to parse JSON from WebSocket message") } - // Wrap in response-like object with ok based on status field - // Status 2xx = ok, 4xx/5xx = not ok, no status = ok (normal response) - const status = parsed.status || 200 - return { - ok: status >= 200 && status < 300, - status, - data: parsed, - } } send_json(data) { diff --git a/frontend/src/utils/passkey.js b/frontend/src/utils/passkey.js index b014aef..1dbe897 100644 --- a/frontend/src/utils/passkey.js +++ b/frontend/src/utils/passkey.js @@ -23,28 +23,28 @@ export async function register(resetToken = null, displayName = null, onstartreg const res = await ws.receive_json() // Handle auth errors (401/403) with iframe - if ((res.status === 401 || res.status === 403) && res.data.auth?.iframe) { + if ((res.status === 401 || res.status === 403) && res.auth?.iframe) { ws.close() - await showAuthIframe(res.data.auth.iframe) + await showAuthIframe(res.auth.iframe) continue } - // Handle other errors - if (!res.ok) { - throw new Error(res.data.detail || `Registration failed: ${res.status}`) + // Handle other errors (status field present means error) + if (res.status) { + throw new Error(res.detail || `Registration failed: ${res.status}`) } // Notify caller that we're about to show the browser prompt if (onstartreg) onstartreg() - const registrationResponse = await startRegistration({ optionsJSON: res.data }) + const registrationResponse = await startRegistration({ optionsJSON: res }) ws.send_json(registrationResponse) const result = await ws.receive_json() - if (!result.ok) { - throw new Error(result.data.detail || `Registration failed: ${result.status}`) + if (result.status) { + throw new Error(result.detail || `Registration failed: ${result.status}`) } - return result.data + return result } catch (error) { ws.close() console.error('Registration error:', error) @@ -58,18 +58,19 @@ export async function authenticate() { const ws = await aWebSocket(await makeUrl('/auth/ws/authenticate')) try { const res = await ws.receive_json() - if (!res.ok) { - throw new Error(res.data.detail || `Authentication failed: ${res.status}`) + // status field present means error + if (res.status) { + throw new Error(res.detail || `Authentication failed: ${res.status}`) } - const authResponse = await startAuthentication({ optionsJSON: res.data }) + const authResponse = await startAuthentication({ optionsJSON: res }) ws.send_json(authResponse) const result = await ws.receive_json() - if (!result.ok) { - throw new Error(result.data.detail || `Authentication failed: ${result.status}`) + if (result.status) { + throw new Error(result.detail || `Authentication failed: ${result.status}`) } - return result.data + return result } catch (error) { console.error('Authentication error:', error) throw Error(error.name === "NotAllowedError" ? 'Passkey authentication cancelled' : error.message) diff --git a/passkey/fastapi/api.py b/passkey/fastapi/api.py index 82ab065..1488449 100644 --- a/passkey/fastapi/api.py +++ b/passkey/fastapi/api.py @@ -228,7 +228,11 @@ async def api_user_info( target_user_uuid = reset_token.user_uuid else: if auth is None: - raise ValueError("Authentication Required") + raise authz.AuthException( + status_code=401, + detail="Authentication required", + mode="login", + ) session_record = await get_session(auth, host=request.headers.get("host")) authenticated = True target_user_uuid = session_record.user_uuid diff --git a/passkey/fastapi/ws.py b/passkey/fastapi/ws.py index 086f113..b9a36f9 100644 --- a/passkey/fastapi/ws.py +++ b/passkey/fastapi/ws.py @@ -30,10 +30,10 @@ def websocket_error_handler(func): } ) except (ValueError, InvalidAuthenticationResponse) as e: - await ws.send_json({"detail": str(e)}) + await ws.send_json({"status": 401, "detail": str(e)}) except Exception: logging.exception("Internal Server Error") - await ws.send_json({"detail": "Internal Server Error"}) + await ws.send_json({"status": 500, "detail": "Internal Server Error"}) return wrapper