Files
eng-pad-server/web/templates/login.html
Kyle Isom 710fcfcd34 Complete WebAuthn web handlers and download real htmx
- Real htmx.min.js (v2.0.4, 50KB) replaces stub
- WebAuthn registration handlers (begin/finish) for adding security keys
- WebAuthn login handlers (begin/finish) for passwordless login
- Key management page (list/delete registered keys)
- Login page updated with "Login with Security Key" button + JS
- Session store for WebAuthn ceremonies (mutex-protected map)
- WebAuthn config passed from server command through to webserver
- Added LookupUserID helper for username-based WebAuthn login

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:33:45 -07:00

117 lines
4.3 KiB
HTML

{{define "title"}}Login — Engineering Pad{{end}}
{{define "content"}}
<h1>Login</h1>
<form method="POST" action="/login">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Login</button>
</form>
<div id="webauthn-login" style="display:none; margin-top:1.5rem;">
<hr style="margin:1rem 0;">
<button type="button" class="btn" id="webauthn-login-btn">Login with Security Key</button>
<p id="webauthn-login-status" style="margin-top:0.5rem;"></p>
</div>
<script>
(function() {
if (!window.PublicKeyCredential) return;
var section = document.getElementById("webauthn-login");
section.style.display = "block";
var btn = document.getElementById("webauthn-login-btn");
var statusEl = document.getElementById("webauthn-login-status");
btn.addEventListener("click", async function() {
var username = document.getElementById("username").value;
if (!username) {
statusEl.style.color = "red";
statusEl.textContent = "Enter your username first.";
return;
}
statusEl.textContent = "";
statusEl.style.color = "";
btn.disabled = true;
btn.textContent = "Waiting for security key...";
try {
var beginResp = await fetch("/webauthn/login/begin", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({username: username})
});
if (!beginResp.ok) {
var err = await beginResp.json();
throw new Error(err.error || "Login failed");
}
var options = await beginResp.json();
options.publicKey.challenge = base64urlToBuffer(options.publicKey.challenge);
if (options.publicKey.allowCredentials) {
options.publicKey.allowCredentials.forEach(function(c) {
c.id = base64urlToBuffer(c.id);
});
}
var assertion = await navigator.credentials.get({publicKey: options.publicKey});
var body = JSON.stringify({
id: assertion.id,
rawId: bufferToBase64url(assertion.rawId),
type: assertion.type,
response: {
authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
signature: bufferToBase64url(assertion.response.signature),
userHandle: assertion.response.userHandle ? bufferToBase64url(assertion.response.userHandle) : ""
}
});
var finishResp = await fetch("/webauthn/login/finish?username=" + encodeURIComponent(username), {
method: "POST",
headers: {"Content-Type": "application/json"},
body: body
});
if (!finishResp.ok) {
var err2 = await finishResp.json();
throw new Error(err2.error || "Login failed");
}
window.location.href = "/notebooks";
} catch(e) {
statusEl.style.color = "red";
statusEl.textContent = e.message || "Login failed";
} finally {
btn.disabled = false;
btn.textContent = "Login with Security Key";
}
});
function base64urlToBuffer(b64url) {
var b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
while (b64.length % 4) b64 += "=";
var binary = atob(b64);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
function bufferToBase64url(buf) {
var bytes = new Uint8Array(buf);
var binary = "";
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
})();
</script>
{{end}}