// webauthn.js — WebAuthn/passkey helpers for the MCIAS web UI. // CSP-compliant: loaded as an external script, no inline code. (function () { 'use strict'; // Base64URL encode/decode helpers for WebAuthn ArrayBuffer <-> JSON transport. function base64urlEncode(buffer) { var bytes = new Uint8Array(buffer); var str = ''; for (var i = 0; i < bytes.length; i++) { str += String.fromCharCode(bytes[i]); } return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } function base64urlDecode(str) { str = str.replace(/-/g, '+').replace(/_/g, '/'); while (str.length % 4) { str += '='; } var binary = atob(str); var bytes = new Uint8Array(binary.length); for (var i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } // Get the CSRF token from the cookie for mutating requests. function getCSRFToken() { var match = document.cookie.match(/(?:^|;\s*)mcias_csrf=([^;]+)/); return match ? match[1] : ''; } function showError(id, msg) { var el = document.getElementById(id); if (el) { el.textContent = msg; el.style.display = ''; } } function hideError(id) { var el = document.getElementById(id); if (el) { el.style.display = 'none'; el.textContent = ''; } } // mciasWebAuthnRegister initiates a passkey/security-key registration. window.mciasWebAuthnRegister = function (password, name, onSuccess, onError) { if (!window.PublicKeyCredential) { onError('WebAuthn is not supported in this browser.'); return; } var csrf = getCSRFToken(); var savedNonce = ''; fetch('/profile/webauthn/begin', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ password: password, name: name }) }) .then(function (resp) { if (!resp.ok) return resp.text().then(function (t) { throw new Error(t || 'Registration failed'); }); return resp.json(); }) .then(function (data) { savedNonce = data.nonce; var opts = data.options; opts.publicKey.challenge = base64urlDecode(opts.publicKey.challenge); if (opts.publicKey.user && opts.publicKey.user.id) { opts.publicKey.user.id = base64urlDecode(opts.publicKey.user.id); } if (opts.publicKey.excludeCredentials) { for (var i = 0; i < opts.publicKey.excludeCredentials.length; i++) { opts.publicKey.excludeCredentials[i].id = base64urlDecode(opts.publicKey.excludeCredentials[i].id); } } return navigator.credentials.create(opts); }) .then(function (credential) { if (!credential) throw new Error('Registration cancelled'); var credJSON = { id: credential.id, rawId: base64urlEncode(credential.rawId), type: credential.type, response: { attestationObject: base64urlEncode(credential.response.attestationObject), clientDataJSON: base64urlEncode(credential.response.clientDataJSON) } }; if (credential.response.getTransports) { credJSON.response.transports = credential.response.getTransports(); } return fetch('/profile/webauthn/finish', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ nonce: savedNonce, name: name, credential: credJSON }) }); }) .then(function (resp) { if (!resp.ok) return resp.text().then(function (t) { throw new Error(t || 'Registration failed'); }); return resp.json(); }) .then(function (result) { onSuccess(result); }) .catch(function (err) { onError(err.message || 'Registration failed'); }); }; // mciasWebAuthnLogin initiates a passkey login. window.mciasWebAuthnLogin = function (username, onSuccess, onError) { if (!window.PublicKeyCredential) { onError('WebAuthn is not supported in this browser.'); return; } var savedNonce = ''; fetch('/login/webauthn/begin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username || '' }) }) .then(function (resp) { if (!resp.ok) return resp.text().then(function (t) { throw new Error(t || 'Login failed'); }); return resp.json(); }) .then(function (data) { savedNonce = data.nonce; var opts = data.options; opts.publicKey.challenge = base64urlDecode(opts.publicKey.challenge); if (opts.publicKey.allowCredentials) { for (var i = 0; i < opts.publicKey.allowCredentials.length; i++) { opts.publicKey.allowCredentials[i].id = base64urlDecode(opts.publicKey.allowCredentials[i].id); } } return navigator.credentials.get(opts); }) .then(function (assertion) { if (!assertion) throw new Error('Login cancelled'); var credJSON = { id: assertion.id, rawId: base64urlEncode(assertion.rawId), type: assertion.type, response: { authenticatorData: base64urlEncode(assertion.response.authenticatorData), clientDataJSON: base64urlEncode(assertion.response.clientDataJSON), signature: base64urlEncode(assertion.response.signature) } }; if (assertion.response.userHandle) { credJSON.response.userHandle = base64urlEncode(assertion.response.userHandle); } return fetch('/login/webauthn/finish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nonce: savedNonce, credential: credJSON }) }); }) .then(function (resp) { if (!resp.ok) return resp.text().then(function (t) { throw new Error(t || 'Login failed'); }); return resp.json(); }) .then(function () { onSuccess(); }) .catch(function (err) { onError(err.message || 'Login failed'); }); }; // Auto-wire the profile page enrollment button. document.addEventListener('DOMContentLoaded', function () { var enrollBtn = document.getElementById('webauthn-enroll-btn'); if (enrollBtn) { enrollBtn.addEventListener('click', function () { var pw = document.getElementById('webauthn-password').value; var name = document.getElementById('webauthn-name').value || 'Passkey'; hideError('webauthn-enroll-error'); hideError('webauthn-enroll-success'); if (!pw) { showError('webauthn-enroll-error', 'Password is required.'); return; } enrollBtn.disabled = true; enrollBtn.textContent = 'Waiting for authenticator...'; window.mciasWebAuthnRegister(pw, name, function () { enrollBtn.disabled = false; enrollBtn.textContent = 'Add Passkey'; document.getElementById('webauthn-password').value = ''; var msg = document.getElementById('webauthn-enroll-success'); if (msg) { msg.textContent = 'Passkey registered successfully.'; msg.style.display = ''; } // Reload the credentials list. window.location.reload(); }, function (err) { enrollBtn.disabled = false; enrollBtn.textContent = 'Add Passkey'; showError('webauthn-enroll-error', err); }); }); } // Auto-wire the login page passkey button. var loginBtn = document.getElementById('webauthn-login-btn'); if (loginBtn) { loginBtn.addEventListener('click', function () { hideError('webauthn-login-error'); loginBtn.disabled = true; loginBtn.textContent = 'Waiting for authenticator...'; window.mciasWebAuthnLogin('', function () { window.location.href = '/dashboard'; }, function (err) { loginBtn.disabled = false; loginBtn.textContent = 'Sign in with passkey'; showError('webauthn-login-error', err); }); }); } }); })();