Brought examples directly to front page.
This commit is contained in:
+142
-101
@@ -4,122 +4,163 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PassKey Auth - Dev Mode</title>
|
<title>PassKey Auth - Dev Mode</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
:root {
|
||||||
margin: 0;
|
color-scheme: light dark; /* Automatic themes by browser */
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
body {
|
/* Login/reauth/forbidden dialog will appear in this iframe */
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
#auth-iframe {
|
||||||
line-height: 1.6;
|
/* Full viewport overlay */
|
||||||
color: #333;
|
border: none;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
position: fixed;
|
||||||
min-height: 100vh;
|
top: 0;
|
||||||
display: flex;
|
left: 0;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 40px;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
/* Optional transparent background with optional blur backdrop */
|
||||||
|
color-scheme: auto;
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: blur(.1rem) brightness(0.7);
|
||||||
|
-webkit-backdrop-filter: blur(.1rem) brightness(0.7);
|
||||||
}
|
}
|
||||||
h1 {
|
/* Prevent background scroll when auth-iframe is shown */
|
||||||
color: #667eea;
|
body:has(#auth-iframe) {
|
||||||
margin-bottom: 10px;
|
overflow: hidden;
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #667eea;
|
|
||||||
margin-top: 30px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
.section:first-of-type h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.links.side-by-side {
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.links.side-by-side a {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
padding: 15px 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
.example-description {
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.9;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.info p {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🔐 PassKey Auth</h1>
|
<header>
|
||||||
<p class="subtitle">Dev server running this example application.</p>
|
<h1>🔐 PassKey Auth</h1>
|
||||||
|
<p class="subtitle">Development server demonstration page.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="section">
|
<div class="content">
|
||||||
<h2>Main Application</h2>
|
<div class="section">
|
||||||
<div class="links side-by-side">
|
<h2>Management Site</h2>
|
||||||
<a href="/auth/">👤 User Profile</a>
|
<button onclick="window.open('/auth/', '_blank')">👤 User Profile</button>
|
||||||
<a href="/auth/admin/">⚙️ Admin Panel</a>
|
<button onclick="window.open('/auth/admin/', '_blank')">⚙️ Admin Panel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Examples & Demos</h2>
|
<h2>API Mode (not leaving the page)</h2>
|
||||||
<div class="links">
|
<p>For SPAs and fetch() calls - shows auth in an iframe overlay:</p>
|
||||||
<a href="/examples/restricted-api.html">
|
<button onclick="apiCall('/auth/api/user-info', 'POST')">📋 Get User Info</button>
|
||||||
📡 API Demo
|
<button onclick="apiCall('/auth/api/forward?max_age=10s')">🔄 Reauth (max_age=10s)</button>
|
||||||
<div class="example-description">Login and re-authentication via JS</div>
|
<button onclick="apiCall('/auth/api/forward?perm=auth:admin')">🛡️ Admin Only</button>
|
||||||
</a>
|
<button onclick="logout()">🚪 Logout</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info">
|
<div class="section">
|
||||||
<p><strong>Dev Server:</strong> Vite on port 4403</p>
|
<h2>Browser Mode (full page)</h2>
|
||||||
<p><strong>Backend:</strong> localhost:4402</p>
|
<p>Block access to otherwise open site - intended for forward-auth mechanism (Caddy, Nxinx):</p>
|
||||||
|
<button onclick="browserNav('/auth/api/forward')">🔐 Basic Auth</button>
|
||||||
|
<button onclick="browserNav('/auth/api/forward?max_age=10s')">🔄 Reauth (max_age=10s)</button>
|
||||||
|
<button onclick="browserNav('/auth/api/forward?perm=auth:admin')">🛡️ Admin Only</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre id="output">Click a button to test...</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
let pendingCall = null; // Stores the API call to retry after auth
|
||||||
|
|
||||||
|
// The auth iframe posts messages when authentication completes or is cancelled.
|
||||||
|
// Message types: 'auth-success' (proceed), 'auth-back' (user cancelled)
|
||||||
|
// Errors during auth stay in the dialog allowing retry, no message is sent.
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
const { type, message } = event.data || {};
|
||||||
|
|
||||||
|
if (type === 'auth-success') {
|
||||||
|
log('✓ Authentication successful, retrying...');
|
||||||
|
hideAuthIframe();
|
||||||
|
// Retry the original API call that triggered authentication
|
||||||
|
if (pendingCall) {
|
||||||
|
const { url, method } = pendingCall;
|
||||||
|
pendingCall = null;
|
||||||
|
apiCall(url, method);
|
||||||
|
}
|
||||||
|
} else if (type === 'auth-back') {
|
||||||
|
log(message || 'Authentication cancelled');
|
||||||
|
hideAuthIframe();
|
||||||
|
pendingCall = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make an API call, handling 401/403 by showing the auth iframe.
|
||||||
|
// The server returns JSON with auth.iframe URL when authentication is needed.
|
||||||
|
async function apiCall(url, method = 'GET') {
|
||||||
|
log(`${method} ${url}...`);
|
||||||
|
|
||||||
|
const response = await fetch(url, { method, credentials: 'include' });
|
||||||
|
|
||||||
|
// Server returns 401 (login/reauth) or 403 (missing permissions)
|
||||||
|
// with a JSON body containing the iframe URL for authentication
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.auth?.iframe) {
|
||||||
|
const mode = data.auth.mode; // 'login' or 'reauth'
|
||||||
|
log(`${mode === 'reauth' ? 'Re-authentication' : 'Authentication'} required...`);
|
||||||
|
pendingCall = { url, method };
|
||||||
|
showAuthIframe(data.auth.iframe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Error: ${response.status} - ${data.detail}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward endpoint returns 204 on success (Caddy then adds Remote-* headers)
|
||||||
|
if (response.status === 204) {
|
||||||
|
log('✓ Success (204 No Content)\nHeaders:\n' +
|
||||||
|
[...response.headers].filter(([k]) => k.startsWith('remote-'))
|
||||||
|
.map(([k, v]) => ` ${k}: ${v}`).join('\n'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log(`Error: ${response.status} ${response.statusText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
log('✓ Response:\n' + JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await fetch('/auth/api/logout', { method: 'POST', credentials: 'include' });
|
||||||
|
log('Logged out');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fullscreen iframe for authentication.
|
||||||
|
// The 'allow' attribute enables WebAuthn (passkey) API inside the iframe.
|
||||||
|
function showAuthIframe(url) {
|
||||||
|
hideAuthIframe();
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.id = 'auth-iframe';
|
||||||
|
iframe.src = url;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
log("Authentication dialog open...")
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAuthIframe() {
|
||||||
|
document.getElementById('auth-iframe')?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
output.textContent = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser mode: open the forward endpoint directly in a new window.
|
||||||
|
// When Accept: text/html, the server redirects to the login page if needed,
|
||||||
|
// then back to the original URL after authentication.
|
||||||
|
function browserNav(url) {
|
||||||
|
log('Opening in new window...\nIf not authenticated, you\'ll see the login page.\nAfter auth, you\'ll see a 204 response (blank page = success).');
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Restricted API Demo</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>🔐 Restricted API Demo</h1>
|
|
||||||
<p class="subtitle">Demonstrates authentication for protected resources</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<div class="section">
|
|
||||||
<h2>API Mode (iframe)</h2>
|
|
||||||
<p>For SPAs and fetch() calls - shows auth in an iframe overlay:</p>
|
|
||||||
<button onclick="apiCall('/auth/api/user-info', 'POST')">Get User Info</button>
|
|
||||||
<button onclick="apiCall('/auth/api/forward?max_age=10s')">Reauth (max_age=10s)</button>
|
|
||||||
<button onclick="apiCall('/auth/api/forward?perm=auth:admin')">Admin Only</button>
|
|
||||||
<button onclick="logout()">Logout</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Browser Mode (full page)</h2>
|
|
||||||
<p>For traditional apps - redirects to auth page, then back:</p>
|
|
||||||
<button onclick="browserNav('/auth/api/forward')">Basic Auth</button>
|
|
||||||
<button onclick="browserNav('/auth/api/forward?max_age=10s')">Reauth (max_age=10s)</button>
|
|
||||||
<button onclick="browserNav('/auth/api/forward?perm=auth:admin')">Admin Only</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre id="output">Click a button to test...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const output = document.getElementById('output');
|
|
||||||
let pendingCall = null;
|
|
||||||
|
|
||||||
// Listen for auth iframe messages
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
const { type, message } = event.data || {};
|
|
||||||
|
|
||||||
if (type === 'auth-success') {
|
|
||||||
log('✓ Authentication successful, retrying...');
|
|
||||||
hideAuthIframe();
|
|
||||||
if (pendingCall) {
|
|
||||||
const { url, method } = pendingCall;
|
|
||||||
pendingCall = null;
|
|
||||||
apiCall(url, method);
|
|
||||||
}
|
|
||||||
} else if (type === 'auth-back' || type === 'auth-error') {
|
|
||||||
log(message || 'Authentication cancelled');
|
|
||||||
hideAuthIframe();
|
|
||||||
pendingCall = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function apiCall(url, method = 'GET') {
|
|
||||||
log(`${method} ${url}...`);
|
|
||||||
|
|
||||||
const response = await fetch(url, { method, credentials: 'include' });
|
|
||||||
|
|
||||||
// If auth required, show the auth iframe
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.auth?.iframe) {
|
|
||||||
const mode = data.auth.mode;
|
|
||||||
log(`${mode === 'reauth' ? 'Re-authentication' : 'Authentication'} required...`);
|
|
||||||
pendingCall = { url, method };
|
|
||||||
showAuthIframe(data.auth.iframe);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log(`Error: ${response.status} - ${data.detail}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
log('✓ Success (204 No Content)\nHeaders:\n' +
|
|
||||||
[...response.headers].filter(([k]) => k.startsWith('remote-'))
|
|
||||||
.map(([k, v]) => ` ${k}: ${v}`).join('\n'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
log(`Error: ${response.status} ${response.statusText}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
log('✓ Response:\n' + JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
await fetch('/auth/api/logout', { method: 'POST', credentials: 'include' });
|
|
||||||
log('Logged out');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAuthIframe(url) {
|
|
||||||
hideAuthIframe();
|
|
||||||
const iframe = document.createElement('iframe');
|
|
||||||
iframe.id = 'auth-iframe';
|
|
||||||
iframe.src = url;
|
|
||||||
iframe.allow = 'publickey-credentials-get; publickey-credentials-create';
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideAuthIframe() {
|
|
||||||
document.getElementById('auth-iframe')?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(msg) {
|
|
||||||
output.textContent = msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Browser mode: navigate directly to the forward endpoint
|
|
||||||
// Browser sends Accept: text/html, so it gets a full page instead of JSON
|
|
||||||
function browserNav(url) {
|
|
||||||
log('Opening in new window...\nIf not authenticated, you\'ll see the login page.\nAfter auth, you\'ll see a 204 response (blank page = success).');
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:has(#auth-iframe) {
|
|
||||||
overflow: hidden; /* prevent scrolling the page */
|
|
||||||
}
|
|
||||||
|
|
||||||
#auth-iframe {
|
|
||||||
border: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 9999;
|
|
||||||
color-scheme: auto;
|
|
||||||
background: transparent;
|
|
||||||
backdrop-filter: blur(.1rem) brightness(0.7);
|
|
||||||
-webkit-backdrop-filter: blur(.1rem) brightness(0.7);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user