Update forward API to return in JSON iframe srcdoc with options injected. (currently broken in dev mode).
This commit is contained in:
@@ -103,7 +103,32 @@
|
||||
}
|
||||
});
|
||||
|
||||
function showLoginMode() {
|
||||
// Cache for auth iframe HTML by mode
|
||||
const authIframeHtmlCache = {};
|
||||
|
||||
async function getAuthIframeHtml(mode = 'login') {
|
||||
if (authIframeHtmlCache[mode]) {
|
||||
return authIframeHtmlCache[mode];
|
||||
}
|
||||
|
||||
// Fetch from forward endpoint - it returns HTML in auth.iframe on 401/403
|
||||
const response = await fetch('/auth/api/forward', { credentials: 'include' });
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
const data = await response.json();
|
||||
if (data.auth?.iframe) {
|
||||
let html = data.auth.iframe;
|
||||
if (mode !== data.auth.mode) {
|
||||
// Replace data-mode attribute value
|
||||
html = html.replace(/data-mode="[^"]*"/, `data-mode="${mode}"`);
|
||||
}
|
||||
authIframeHtmlCache[mode] = html;
|
||||
return html;
|
||||
}
|
||||
}
|
||||
throw new Error('Unable to fetch auth iframe HTML');
|
||||
}
|
||||
|
||||
async function showLoginMode() {
|
||||
let iframe = document.getElementById('auth-iframe');
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
@@ -111,11 +136,11 @@
|
||||
iframe.title = 'Authentication';
|
||||
document.body.appendChild(iframe);
|
||||
}
|
||||
iframe.src = '/auth/restricted/?mode=login';
|
||||
iframe.srcdoc = await getAuthIframeHtml('login');
|
||||
showStatus('Login mode loaded - for users who are not authenticated', 'info');
|
||||
}
|
||||
|
||||
function showReauthMode() {
|
||||
async function showReauthMode() {
|
||||
let iframe = document.getElementById('auth-iframe');
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
@@ -123,17 +148,17 @@
|
||||
iframe.title = 'Authentication';
|
||||
document.body.appendChild(iframe);
|
||||
}
|
||||
iframe.src = '/auth/restricted/?mode=reauth';
|
||||
iframe.srcdoc = await getAuthIframeHtml('reauth');
|
||||
showStatus('Reauth mode loaded - for additional verification of authenticated users', 'info');
|
||||
}
|
||||
|
||||
function showAuthIframe() {
|
||||
async function showAuthIframe() {
|
||||
let iframe = document.getElementById('auth-iframe');
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.id = 'auth-iframe';
|
||||
iframe.title = 'Authentication';
|
||||
iframe.src = '/auth/restricted/';
|
||||
iframe.srcdoc = await getAuthIframeHtml('login');
|
||||
document.body.appendChild(iframe);
|
||||
iframeInitialized = true;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { apiJson } from '@/utils/api'
|
||||
import { apiJson, getAuthIframeHtml } from '@/utils/api'
|
||||
import StatusMessage from '@/components/StatusMessage.vue'
|
||||
import ProfileView from '@/components/ProfileView.vue'
|
||||
import LoadingView from '@/components/LoadingView.vue'
|
||||
@@ -39,15 +39,16 @@ async function tryLoadUserInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
function showAuthIframe() {
|
||||
async function showAuthIframe() {
|
||||
// Remove existing iframe if any
|
||||
hideAuthIframe()
|
||||
|
||||
// Create new iframe for authentication
|
||||
// Create new iframe for authentication using srcdoc
|
||||
const html = await getAuthIframeHtml('login')
|
||||
authIframe = document.createElement('iframe')
|
||||
authIframe.id = 'auth-iframe'
|
||||
authIframe.title = 'Authentication'
|
||||
authIframe.src = '/auth/restricted/?mode=login'
|
||||
authIframe.srcdoc = html
|
||||
document.body.appendChild(authIframe)
|
||||
loadingMessage.value = 'Authentication required...'
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import AdminUserDetail from '@/admin/AdminUserDetail.vue'
|
||||
import AdminDialogs from '@/admin/AdminDialogs.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
|
||||
import { apiJson } from '@/utils/api'
|
||||
import { apiJson, getAuthIframeHtml } from '@/utils/api'
|
||||
|
||||
const info = ref(null)
|
||||
const loading = ref(true)
|
||||
@@ -288,12 +288,13 @@ function deletePermission(p) {
|
||||
} })
|
||||
}
|
||||
|
||||
function showAuthIframe() {
|
||||
async function showAuthIframe() {
|
||||
hideAuthIframe()
|
||||
const html = await getAuthIframeHtml('login')
|
||||
authIframe = document.createElement('iframe')
|
||||
authIframe.id = 'auth-iframe'
|
||||
authIframe.title = 'Authentication'
|
||||
authIframe.src = '/auth/restricted/?mode=login'
|
||||
authIframe.srcdoc = html
|
||||
document.body.appendChild(authIframe)
|
||||
loadingMessage.value = 'Authentication required...'
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
import RestrictedAuth from '@/components/RestrictedAuth.vue'
|
||||
|
||||
// Detect mode from data attribute on html tag (injected by server)
|
||||
const authMode = computed(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
return params.get('mode') === 'reauth' ? 'reauth' : 'login'
|
||||
const htmlElement = document.documentElement
|
||||
const dataMode = htmlElement.getAttribute('data-mode')
|
||||
if (dataMode === 'reauth') return 'reauth'
|
||||
if (dataMode === 'forbidden') return 'forbidden'
|
||||
return 'login'
|
||||
})
|
||||
|
||||
function postToParent(message) {
|
||||
|
||||
@@ -49,6 +49,40 @@ let authPromise = null
|
||||
let authResolve = null
|
||||
let authReject = null
|
||||
|
||||
// Cache for auth iframe HTML by mode
|
||||
const authIframeHtmlCache = {}
|
||||
|
||||
/**
|
||||
* Get the auth iframe HTML for a given mode.
|
||||
* Fetches from /auth/api/forward which returns HTML in the auth.iframe field.
|
||||
* Results are cached per mode.
|
||||
* @param {string} mode - The auth mode ('login', 'reauth', 'forbidden')
|
||||
* @returns {Promise<string>} - The HTML content for the iframe
|
||||
*/
|
||||
export async function getAuthIframeHtml(mode = 'login') {
|
||||
if (authIframeHtmlCache[mode]) {
|
||||
return authIframeHtmlCache[mode]
|
||||
}
|
||||
|
||||
// Fetch from forward endpoint - it returns HTML in auth.iframe on 401/403
|
||||
const response = await fetch('/auth/api/forward', { credentials: 'include' })
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
const data = await response.json()
|
||||
if (data.auth?.iframe) {
|
||||
// Cache the HTML - it's the same regardless of mode (mode is in data attrs)
|
||||
// But we need to patch the mode in the HTML if different from returned
|
||||
let html = data.auth.iframe
|
||||
if (mode !== data.auth.mode) {
|
||||
// Replace data-mode attribute value
|
||||
html = html.replace(/data-mode="[^"]*"/, `data-mode="${mode}"`)
|
||||
}
|
||||
authIframeHtmlCache[mode] = html
|
||||
return html
|
||||
}
|
||||
}
|
||||
throw new Error('Unable to fetch auth iframe HTML')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an auth iframe is already open (from any source).
|
||||
* @returns {boolean}
|
||||
@@ -60,11 +94,11 @@ export function isAuthIframeOpen() {
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} iframeSrc - The URL for the iframe src
|
||||
* @param {string} iframeHtml - The HTML content for the iframe srcdoc
|
||||
* @returns {Promise<void>}
|
||||
* @throws {AuthCancelledError} - If authentication is cancelled by user
|
||||
*/
|
||||
export function showAuthIframe(iframeSrc) {
|
||||
export function showAuthIframe(iframeHtml) {
|
||||
// If we already have a promise (from us), return it
|
||||
if (authPromise) return authPromise
|
||||
|
||||
@@ -86,11 +120,11 @@ export function showAuthIframe(iframeSrc) {
|
||||
// Remove existing iframe if any
|
||||
hideAuthIframe()
|
||||
|
||||
// Create new iframe for authentication
|
||||
// Create new iframe for authentication using srcdoc
|
||||
authIframe = document.createElement('iframe')
|
||||
authIframe.id = 'auth-iframe'
|
||||
authIframe.title = 'Authentication'
|
||||
authIframe.src = iframeSrc
|
||||
authIframe.srcdoc = iframeHtml
|
||||
document.body.appendChild(authIframe)
|
||||
|
||||
return authPromise
|
||||
|
||||
@@ -33,14 +33,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
|
||||
"""Handle AuthException with auth info for UI."""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"detail": exc.detail,
|
||||
"auth": {
|
||||
"mode": exc.mode,
|
||||
"iframe": f"/auth/restricted/?mode={exc.mode}",
|
||||
**exc.metadata,
|
||||
},
|
||||
},
|
||||
content=authz.auth_error_content(exc),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+5
-21
@@ -62,14 +62,7 @@ async def auth_exception_handler(_request: Request, exc: authz.AuthException):
|
||||
"""Handle AuthException with auth info for UI."""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"detail": exc.detail,
|
||||
"auth": {
|
||||
"mode": exc.mode,
|
||||
"iframe": f"/auth/restricted/?mode={exc.mode}",
|
||||
**exc.metadata,
|
||||
},
|
||||
},
|
||||
content=authz.auth_error_content(exc),
|
||||
)
|
||||
|
||||
|
||||
@@ -184,27 +177,18 @@ async def forward_authentication(
|
||||
wants_html = "text/html" in accept
|
||||
|
||||
if wants_html:
|
||||
# Browser request - return HTML with metadata
|
||||
html = frontend.file("int", "forward", "index.html").read_bytes()
|
||||
# Inject mode and any additional metadata
|
||||
# Browser request - return full-page HTML with metadata
|
||||
data_attrs = {"mode": e.mode, **e.metadata}
|
||||
html = frontend.file("int", "forward", "index.html").read_bytes()
|
||||
html = htmlutil.patch_html_data_attrs(html, **data_attrs)
|
||||
return Response(
|
||||
html, status_code=e.status_code, media_type="text/html; charset=UTF-8"
|
||||
)
|
||||
else:
|
||||
# API request - return JSON with iframe src link
|
||||
iframe_url = f"/auth/restricted/?mode={e.mode}"
|
||||
# API request - return JSON with iframe srcdoc HTML
|
||||
return JSONResponse(
|
||||
status_code=e.status_code,
|
||||
content={
|
||||
"detail": e.detail,
|
||||
"auth": {
|
||||
"mode": e.mode,
|
||||
"iframe": iframe_url,
|
||||
**e.metadata,
|
||||
},
|
||||
},
|
||||
content=authz.auth_error_content(e),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from ..util import permutil, sessionutil
|
||||
from ..util import frontend, htmlutil, permutil, sessionutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,6 +32,24 @@ class AuthException(HTTPException):
|
||||
self.metadata = metadata
|
||||
|
||||
|
||||
def auth_error_content(exc: AuthException) -> dict:
|
||||
"""Generate JSON response content for an AuthException.
|
||||
|
||||
Returns a dict with detail, mode, and iframe HTML for srcdoc embedding.
|
||||
"""
|
||||
data_attrs = {"mode": exc.mode, **exc.metadata}
|
||||
iframe_html = frontend.file("auth", "restricted", "index.html").read_bytes()
|
||||
iframe_html = htmlutil.patch_html_data_attrs(iframe_html, **data_attrs)
|
||||
return {
|
||||
"detail": exc.detail,
|
||||
"auth": {
|
||||
"mode": exc.mode,
|
||||
"iframe": iframe_html.decode("utf-8"),
|
||||
**exc.metadata,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def verify(
|
||||
auth: str | None,
|
||||
perm: list[str],
|
||||
|
||||
@@ -29,14 +29,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
|
||||
"""Handle AuthException with auth info for UI."""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"detail": exc.detail,
|
||||
"auth": {
|
||||
"mode": exc.mode,
|
||||
"iframe": f"/auth/restricted/?mode={exc.mode}",
|
||||
**exc.metadata,
|
||||
},
|
||||
},
|
||||
content=authz.auth_error_content(exc),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,12 +26,7 @@ def websocket_error_handler(func):
|
||||
await ws.send_json(
|
||||
{
|
||||
"status": e.status_code,
|
||||
"detail": e.detail,
|
||||
"auth": {
|
||||
"mode": e.mode,
|
||||
"iframe": f"/auth/restricted/?mode={e.mode}",
|
||||
**e.metadata,
|
||||
},
|
||||
**authz.auth_error_content(e),
|
||||
}
|
||||
)
|
||||
except (ValueError, InvalidAuthenticationResponse) as e:
|
||||
|
||||
Reference in New Issue
Block a user