auth-yes/ui/components/LoginPage.tsx
google-labs-jules[bot] e1555f14fc feat: implement WebAuthn PRF extension for client-side HKDF key derivation
* Added `prf_enabled` and `prf_salt` columns to the `passkeys` table.
* Updated registration API endpoints to request and store the PRF extension capability and generate a secure salt.
* Updated the login API endpoints to map stored PRF salts into the `evalByCredential` array for the WebAuthn challenge.
* Enhanced the client-side WebAuthn SDK (`auth-client.js`) to extract the PRF Base64URL string output, decode it into a `Uint8Array`, and securely derive a 256-bit AES-GCM Key Encryption Key (KEK) via `crypto.subtle.deriveKey` using the `auth-yes:prf:device-share:v1` info string.
* Implemented graceful fallbacks throughout the stack to ensure registration and standard logins proceed if PRF is unsupported.
* Added corresponding unit tests to verify PRF flow and rejection logic.
* Verified visual and functional changes for the optional username input on the login page via Playwright scripts.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-24 07:46:22 +00:00

122 lines
3.6 KiB
TypeScript

import { Layout } from "./Layout.tsx";
export const LoginPage = () => {
return (
<Layout title="Login">
<div style={{ textAlign: "center" }}>
<h1 style={{ marginBottom: "0.5rem" }}>Authenticate</h1>
<p style={{ color: "#666", marginBottom: "2rem" }}>
Use your registered hardware key or passkey to log in.
</p>
<div
id="instructionBox"
style={{
background: "#eef2f5",
padding: "1rem",
borderRadius: "6px",
marginBottom: "1.5rem",
fontSize: "0.9rem",
color: "#333",
border: "1px solid #dcdcdc",
}}
>
<p style={{ margin: "0 0 0.5rem 0" }}>
<strong>Instruction:</strong>
</p>
<ul style={{ margin: 0, paddingLeft: "1.5rem", textAlign: "left" }}>
<li>
Insert your hardware token (e.g. YubiKey) into the USB port.
</li>
<li>
Or prepare to scan a QR code if using a mobile device passkey.
</li>
</ul>
</div>
<div style={{ marginBottom: "1rem" }}>
<input
type="text"
id="loginUsername"
placeholder="Username (optional for passkeys)"
style={{
padding: "0.5rem",
width: "100%",
maxWidth: "300px",
borderRadius: "4px",
border: "1px solid #ccc"
}}
/>
</div>
<button
type="button"
id="loginBtn"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "0.5rem",
fontWeight: "bold",
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
Login with Passkey
</button>
<div
id="loadingIndicator"
style={{
display: "none",
marginTop: "1rem",
color: "#007bff",
fontSize: "0.9rem",
}}
>
Waiting for authenticator... Please follow the prompt.
</div>
<div id="statusMessage" class="error"></div>
<div class="links" style={{ marginTop: "2rem" }}>
Don't have an account? <a href="/register">Register here</a>
</div>
</div>
<script src="/public/auth-client.js"></script>
<script
dangerouslySetInnerHTML={{
__html: `
document.getElementById('loginBtn').addEventListener('click', async () => {
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('loginBtn').disabled = true;
document.getElementById('statusMessage').textContent = '';
try {
const username = document.getElementById('loginUsername').value;
await startWebAuthnLogin(username);
} finally {
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('loginBtn').disabled = false;
}
});
`,
}}
>
</script>
</Layout>
);
};