Revert earlier change to iframe srcdoc, using src instead, because srcdoc was not compatible with all passkey implementations (BitWarden).
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { apiJson, getAuthIframeHtml } from '@/utils/api'
|
import { apiJson, getAuthIframeUrl } from '@/utils/api'
|
||||||
import StatusMessage from '@/components/StatusMessage.vue'
|
import StatusMessage from '@/components/StatusMessage.vue'
|
||||||
import ProfileView from '@/components/ProfileView.vue'
|
import ProfileView from '@/components/ProfileView.vue'
|
||||||
import LoadingView from '@/components/LoadingView.vue'
|
import LoadingView from '@/components/LoadingView.vue'
|
||||||
@@ -42,12 +42,13 @@ async function showAuthIframe() {
|
|||||||
// Remove existing iframe if any
|
// Remove existing iframe if any
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
|
|
||||||
// Create new iframe for authentication using srcdoc
|
// Create new iframe for authentication using src URL
|
||||||
const html = await getAuthIframeHtml('login')
|
const url = await getAuthIframeUrl('login')
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.srcdoc = html
|
authIframe.allow = 'publickey-credentials-get; publickey-credentials-create'
|
||||||
|
authIframe.src = url
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
loadingMessage.value = 'Authentication required...'
|
loadingMessage.value = 'Authentication required...'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import AdminUserDetail from '@/admin/AdminUserDetail.vue'
|
|||||||
import AdminDialogs from '@/admin/AdminDialogs.vue'
|
import AdminDialogs from '@/admin/AdminDialogs.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
||||||
import { apiJson, getAuthIframeHtml } from '@/utils/api'
|
import { apiJson, getAuthIframeUrl } from '@/utils/api'
|
||||||
|
|
||||||
const info = ref(null)
|
const info = ref(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -299,11 +299,12 @@ function deletePermission(p) {
|
|||||||
|
|
||||||
async function showAuthIframe() {
|
async function showAuthIframe() {
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
const html = await getAuthIframeHtml('login')
|
const url = await getAuthIframeUrl('login')
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.srcdoc = html
|
authIframe.allow = 'publickey-credentials-get; publickey-credentials-create'
|
||||||
|
authIframe.src = url
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
loadingMessage.value = 'Authentication required...'
|
loadingMessage.value = 'Authentication required...'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import RestrictedAuth from '@/components/RestrictedAuth.vue'
|
import RestrictedAuth from '@/components/RestrictedAuth.vue'
|
||||||
|
|
||||||
// Detect mode from data attribute on html tag (injected by server)
|
// Detect mode from URL hash fragment
|
||||||
const authMode = computed(() => {
|
const authMode = computed(() => {
|
||||||
const htmlElement = document.documentElement
|
const params = new URLSearchParams(window.location.hash.slice(1))
|
||||||
const dataMode = htmlElement.getAttribute('data-mode')
|
const mode = params.get('mode')
|
||||||
if (dataMode === 'reauth') return 'reauth'
|
if (mode === 'reauth') return 'reauth'
|
||||||
if (dataMode === 'forbidden') return 'forbidden'
|
if (mode === 'forbidden') return 'forbidden'
|
||||||
return 'login'
|
return 'login'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+21
-21
@@ -49,38 +49,37 @@ let authPromise = null
|
|||||||
let authResolve = null
|
let authResolve = null
|
||||||
let authReject = null
|
let authReject = null
|
||||||
|
|
||||||
// Cache for auth iframe HTML by mode
|
// Cache for auth iframe URL by mode
|
||||||
const authIframeHtmlCache = {}
|
const authIframeUrlCache = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the auth iframe HTML for a given mode.
|
* Get the auth iframe URL for a given mode.
|
||||||
* Fetches from /auth/api/forward which returns HTML in the auth.iframe field.
|
* Fetches from /auth/api/forward which returns URL in the auth.iframe field.
|
||||||
* Results are cached per mode.
|
* Results are cached per mode.
|
||||||
* @param {string} mode - The auth mode ('login', 'reauth', 'forbidden')
|
* @param {string} mode - The auth mode ('login', 'reauth', 'forbidden')
|
||||||
* @returns {Promise<string>} - The HTML content for the iframe
|
* @returns {Promise<string>} - The URL for the iframe
|
||||||
*/
|
*/
|
||||||
export async function getAuthIframeHtml(mode = 'login') {
|
export async function getAuthIframeUrl(mode = 'login') {
|
||||||
if (authIframeHtmlCache[mode]) {
|
if (authIframeUrlCache[mode]) {
|
||||||
return authIframeHtmlCache[mode]
|
return authIframeUrlCache[mode]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch from forward endpoint - it returns HTML in auth.iframe on 401/403
|
// Fetch from forward endpoint - it returns URL in auth.iframe on 401/403
|
||||||
const response = await fetch('/auth/api/forward', { credentials: 'include' })
|
const response = await fetch('/auth/api/forward', { credentials: 'include' })
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (data.auth?.iframe) {
|
if (data.auth?.iframe) {
|
||||||
// Cache the HTML - it's the same regardless of mode (mode is in data attrs)
|
// The iframe field now contains a URL with hash fragment
|
||||||
// But we need to patch the mode in the HTML if different from returned
|
// If mode differs, update the hash param
|
||||||
let html = data.auth.iframe
|
let url = data.auth.iframe
|
||||||
if (mode !== data.auth.mode) {
|
if (mode !== data.auth.mode) {
|
||||||
// Replace data-mode attribute value
|
url = url.replace(/mode=[^&]*/, `mode=${mode}`)
|
||||||
html = html.replace(/data-mode="[^"]*"/, `data-mode="${mode}"`)
|
|
||||||
}
|
}
|
||||||
authIframeHtmlCache[mode] = html
|
authIframeUrlCache[mode] = url
|
||||||
return html
|
return url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error('Unable to fetch auth iframe HTML')
|
throw new Error('Unable to fetch auth iframe URL')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,11 +93,11 @@ export function isAuthIframeOpen() {
|
|||||||
/**
|
/**
|
||||||
* Show the authentication iframe and return a promise that resolves on success.
|
* Show the authentication iframe and return a promise that resolves on success.
|
||||||
* If an auth iframe is already open (from any source), hooks into its completion.
|
* If an auth iframe is already open (from any source), hooks into its completion.
|
||||||
* @param {string} iframeHtml - The HTML content for the iframe srcdoc
|
* @param {string} iframeUrl - The URL for the iframe src
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {AuthCancelledError} - If authentication is cancelled by user
|
* @throws {AuthCancelledError} - If authentication is cancelled by user
|
||||||
*/
|
*/
|
||||||
export function showAuthIframe(iframeHtml) {
|
export function showAuthIframe(iframeUrl) {
|
||||||
// If we already have a promise (from us), return it
|
// If we already have a promise (from us), return it
|
||||||
if (authPromise) return authPromise
|
if (authPromise) return authPromise
|
||||||
|
|
||||||
@@ -120,11 +119,12 @@ export function showAuthIframe(iframeHtml) {
|
|||||||
// Remove existing iframe if any
|
// Remove existing iframe if any
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
|
|
||||||
// Create new iframe for authentication using srcdoc
|
// Create new iframe for authentication using src URL
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.srcdoc = iframeHtml
|
authIframe.allow = 'publickey-credentials-get; publickey-credentials-create'
|
||||||
|
authIframe.src = iframeUrl
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
|
|
||||||
return authPromise
|
return authPromise
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function register(resetToken = null, displayName = null, onstartreg
|
|||||||
// Notify caller that we're about to show the browser prompt
|
// Notify caller that we're about to show the browser prompt
|
||||||
if (onstartreg) onstartreg()
|
if (onstartreg) onstartreg()
|
||||||
|
|
||||||
const registrationResponse = await startRegistration({ optionsJSON: res })
|
const registrationResponse = await startRegistration(res)
|
||||||
ws.send_json(registrationResponse)
|
ws.send_json(registrationResponse)
|
||||||
|
|
||||||
const result = await ws.receive_json()
|
const result = await ws.receive_json()
|
||||||
@@ -64,7 +64,7 @@ export async function authenticate() {
|
|||||||
throw new Error(res.detail || `Authentication failed: ${res.status}`)
|
throw new Error(res.detail || `Authentication failed: ${res.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authResponse = await startAuthentication({ optionsJSON: res })
|
const authResponse = await startAuthentication(res)
|
||||||
ws.send_json(authResponse)
|
ws.send_json(authResponse)
|
||||||
|
|
||||||
const result = await ws.receive_json()
|
const result = await ws.receive_json()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from ..util import frontend, htmlutil, permutil, sessionutil
|
from ..util import permutil, sessionutil
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -35,16 +35,17 @@ class AuthException(HTTPException):
|
|||||||
async def auth_error_content(exc: AuthException) -> dict:
|
async def auth_error_content(exc: AuthException) -> dict:
|
||||||
"""Generate JSON response content for an AuthException.
|
"""Generate JSON response content for an AuthException.
|
||||||
|
|
||||||
Returns a dict with detail, mode, and iframe HTML for srcdoc embedding.
|
Returns a dict with detail, mode, and iframe URL for src embedding.
|
||||||
"""
|
"""
|
||||||
data_attrs = {"mode": exc.mode, **exc.metadata}
|
# Build hash fragment from mode and metadata
|
||||||
iframe_html = (await frontend.read("/auth/restricted/index.html"))[0]
|
params = {"mode": exc.mode, **exc.metadata}
|
||||||
iframe_html = htmlutil.patch_html_data_attrs(iframe_html, **data_attrs)
|
fragment = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
|
||||||
|
iframe_url = f"/auth/restricted/#{fragment}"
|
||||||
return {
|
return {
|
||||||
"detail": exc.detail,
|
"detail": exc.detail,
|
||||||
"auth": {
|
"auth": {
|
||||||
"mode": exc.mode,
|
"mode": exc.mode,
|
||||||
"iframe": iframe_html.decode("utf-8"),
|
"iframe": iframe_url,
|
||||||
**exc.metadata,
|
**exc.metadata,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async def register_chat(
|
|||||||
credential_ids=credential_ids,
|
credential_ids=credential_ids,
|
||||||
origin=origin,
|
origin=origin,
|
||||||
)
|
)
|
||||||
await ws.send_json(options)
|
await ws.send_json({"optionsJSON": options})
|
||||||
response = await ws.receive_json()
|
response = await ws.receive_json()
|
||||||
return passkey.instance.reg_verify(response, challenge, user_uuid, origin=origin)
|
return passkey.instance.reg_verify(response, challenge, user_uuid, origin=origin)
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
|
|||||||
options, challenge = passkey.instance.auth_generate_options(
|
options, challenge = passkey.instance.auth_generate_options(
|
||||||
credential_ids=credential_ids
|
credential_ids=credential_ids
|
||||||
)
|
)
|
||||||
await ws.send_json(options)
|
await ws.send_json({"optionsJSON": options})
|
||||||
# Wait for the client to use his authenticator to authenticate
|
# Wait for the client to use his authenticator to authenticate
|
||||||
credential = passkey.instance.auth_parse(await ws.receive_json())
|
credential = passkey.instance.auth_parse(await ws.receive_json())
|
||||||
# Fetch from the database by credential ID
|
# Fetch from the database by credential ID
|
||||||
|
|||||||
+1
-1
@@ -184,7 +184,7 @@ class Passkey:
|
|||||||
authopts: Additional arguments to generate_authentication_options.
|
authopts: Additional arguments to generate_authentication_options.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (JSON to be sent to client, challenge bytes to store)
|
Tuple of (JSON dict to be sent to client, challenge bytes to store)
|
||||||
"""
|
"""
|
||||||
options = generate_authentication_options(
|
options = generate_authentication_options(
|
||||||
rp_id=self.rp_id,
|
rp_id=self.rp_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user