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');
|
let iframe = document.getElementById('auth-iframe');
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
iframe = document.createElement('iframe');
|
iframe = document.createElement('iframe');
|
||||||
@@ -111,11 +136,11 @@
|
|||||||
iframe.title = 'Authentication';
|
iframe.title = 'Authentication';
|
||||||
document.body.appendChild(iframe);
|
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');
|
showStatus('Login mode loaded - for users who are not authenticated', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showReauthMode() {
|
async function showReauthMode() {
|
||||||
let iframe = document.getElementById('auth-iframe');
|
let iframe = document.getElementById('auth-iframe');
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
iframe = document.createElement('iframe');
|
iframe = document.createElement('iframe');
|
||||||
@@ -123,17 +148,17 @@
|
|||||||
iframe.title = 'Authentication';
|
iframe.title = 'Authentication';
|
||||||
document.body.appendChild(iframe);
|
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');
|
showStatus('Reauth mode loaded - for additional verification of authenticated users', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAuthIframe() {
|
async function showAuthIframe() {
|
||||||
let iframe = document.getElementById('auth-iframe');
|
let iframe = document.getElementById('auth-iframe');
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
iframe = document.createElement('iframe');
|
iframe = document.createElement('iframe');
|
||||||
iframe.id = 'auth-iframe';
|
iframe.id = 'auth-iframe';
|
||||||
iframe.title = 'Authentication';
|
iframe.title = 'Authentication';
|
||||||
iframe.src = '/auth/restricted/';
|
iframe.srcdoc = await getAuthIframeHtml('login');
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
iframeInitialized = true;
|
iframeInitialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } from '@/utils/api'
|
import { apiJson, getAuthIframeHtml } 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'
|
||||||
@@ -39,15 +39,16 @@ async function tryLoadUserInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAuthIframe() {
|
async function showAuthIframe() {
|
||||||
// Remove existing iframe if any
|
// Remove existing iframe if any
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
|
|
||||||
// Create new iframe for authentication
|
// Create new iframe for authentication using srcdoc
|
||||||
|
const html = await getAuthIframeHtml('login')
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.src = '/auth/restricted/?mode=login'
|
authIframe.srcdoc = html
|
||||||
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 } from '@/utils/api'
|
import { apiJson, getAuthIframeHtml } from '@/utils/api'
|
||||||
|
|
||||||
const info = ref(null)
|
const info = ref(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -288,12 +288,13 @@ function deletePermission(p) {
|
|||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAuthIframe() {
|
async function showAuthIframe() {
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
|
const html = await getAuthIframeHtml('login')
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.src = '/auth/restricted/?mode=login'
|
authIframe.srcdoc = html
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
loadingMessage.value = 'Authentication required...'
|
loadingMessage.value = 'Authentication required...'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,13 @@
|
|||||||
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)
|
||||||
const authMode = computed(() => {
|
const authMode = computed(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const htmlElement = document.documentElement
|
||||||
return params.get('mode') === 'reauth' ? 'reauth' : 'login'
|
const dataMode = htmlElement.getAttribute('data-mode')
|
||||||
|
if (dataMode === 'reauth') return 'reauth'
|
||||||
|
if (dataMode === 'forbidden') return 'forbidden'
|
||||||
|
return 'login'
|
||||||
})
|
})
|
||||||
|
|
||||||
function postToParent(message) {
|
function postToParent(message) {
|
||||||
|
|||||||
@@ -49,6 +49,40 @@ let authPromise = null
|
|||||||
let authResolve = null
|
let authResolve = null
|
||||||
let authReject = 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).
|
* Check if an auth iframe is already open (from any source).
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -60,11 +94,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} iframeSrc - The URL for the iframe src
|
* @param {string} iframeHtml - The HTML content for the iframe srcdoc
|
||||||
* @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(iframeSrc) {
|
export function showAuthIframe(iframeHtml) {
|
||||||
// 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
|
||||||
|
|
||||||
@@ -86,11 +120,11 @@ export function showAuthIframe(iframeSrc) {
|
|||||||
// Remove existing iframe if any
|
// Remove existing iframe if any
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
|
|
||||||
// Create new iframe for authentication
|
// Create new iframe for authentication using srcdoc
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = 'auth-iframe'
|
authIframe.id = 'auth-iframe'
|
||||||
authIframe.title = 'Authentication'
|
authIframe.title = 'Authentication'
|
||||||
authIframe.src = iframeSrc
|
authIframe.srcdoc = iframeHtml
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
|
|
||||||
return authPromise
|
return authPromise
|
||||||
|
|||||||
@@ -33,14 +33,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
|
|||||||
"""Handle AuthException with auth info for UI."""
|
"""Handle AuthException with auth info for UI."""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
content={
|
content=authz.auth_error_content(exc),
|
||||||
"detail": exc.detail,
|
|
||||||
"auth": {
|
|
||||||
"mode": exc.mode,
|
|
||||||
"iframe": f"/auth/restricted/?mode={exc.mode}",
|
|
||||||
**exc.metadata,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-21
@@ -62,14 +62,7 @@ async def auth_exception_handler(_request: Request, exc: authz.AuthException):
|
|||||||
"""Handle AuthException with auth info for UI."""
|
"""Handle AuthException with auth info for UI."""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
content={
|
content=authz.auth_error_content(exc),
|
||||||
"detail": exc.detail,
|
|
||||||
"auth": {
|
|
||||||
"mode": exc.mode,
|
|
||||||
"iframe": f"/auth/restricted/?mode={exc.mode}",
|
|
||||||
**exc.metadata,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -184,27 +177,18 @@ async def forward_authentication(
|
|||||||
wants_html = "text/html" in accept
|
wants_html = "text/html" in accept
|
||||||
|
|
||||||
if wants_html:
|
if wants_html:
|
||||||
# Browser request - return HTML with metadata
|
# Browser request - return full-page HTML with metadata
|
||||||
html = frontend.file("int", "forward", "index.html").read_bytes()
|
|
||||||
# Inject mode and any additional metadata
|
|
||||||
data_attrs = {"mode": e.mode, **e.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)
|
html = htmlutil.patch_html_data_attrs(html, **data_attrs)
|
||||||
return Response(
|
return Response(
|
||||||
html, status_code=e.status_code, media_type="text/html; charset=UTF-8"
|
html, status_code=e.status_code, media_type="text/html; charset=UTF-8"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# API request - return JSON with iframe src link
|
# API request - return JSON with iframe srcdoc HTML
|
||||||
iframe_url = f"/auth/restricted/?mode={e.mode}"
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=e.status_code,
|
status_code=e.status_code,
|
||||||
content={
|
content=authz.auth_error_content(e),
|
||||||
"detail": e.detail,
|
|
||||||
"auth": {
|
|
||||||
"mode": e.mode,
|
|
||||||
"iframe": iframe_url,
|
|
||||||
**e.metadata,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from ..util import permutil, sessionutil
|
from ..util import frontend, htmlutil, permutil, sessionutil
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -32,6 +32,24 @@ class AuthException(HTTPException):
|
|||||||
self.metadata = metadata
|
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(
|
async def verify(
|
||||||
auth: str | None,
|
auth: str | None,
|
||||||
perm: list[str],
|
perm: list[str],
|
||||||
|
|||||||
@@ -29,14 +29,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
|
|||||||
"""Handle AuthException with auth info for UI."""
|
"""Handle AuthException with auth info for UI."""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
content={
|
content=authz.auth_error_content(exc),
|
||||||
"detail": exc.detail,
|
|
||||||
"auth": {
|
|
||||||
"mode": exc.mode,
|
|
||||||
"iframe": f"/auth/restricted/?mode={exc.mode}",
|
|
||||||
**exc.metadata,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,7 @@ def websocket_error_handler(func):
|
|||||||
await ws.send_json(
|
await ws.send_json(
|
||||||
{
|
{
|
||||||
"status": e.status_code,
|
"status": e.status_code,
|
||||||
"detail": e.detail,
|
**authz.auth_error_content(e),
|
||||||
"auth": {
|
|
||||||
"mode": e.mode,
|
|
||||||
"iframe": f"/auth/restricted/?mode={e.mode}",
|
|
||||||
**e.metadata,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except (ValueError, InvalidAuthenticationResponse) as e:
|
except (ValueError, InvalidAuthenticationResponse) as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user