- 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>
117 lines
4.3 KiB
HTML
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}}
|