/* DictIA WebAuthn client (B-2.6). * No external dependencies. Wraps the navigator.credentials API and * exchanges base64url-encoded payloads with the Flask backend at * /2fa/passkey/* endpoints. * * Exports window.DictIAWebAuthn = { wireRegisterButton, wireAuthButton }. */ (function (global) { 'use strict'; // --- base64url helpers (no padding) ----------------------------------- function b64urlToBuffer(s) { if (!s) return new ArrayBuffer(0); const pad = '='.repeat((4 - (s.length % 4)) % 4); const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/'); const binary = atob(b64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes.buffer; } function bufferToB64url(buf) { const bytes = new Uint8Array(buf); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // --- Options decoding (server sends b64url; navigator.credentials needs ArrayBuffer) function decodeRegistrationOptions(o) { return Object.assign({}, o, { challenge: b64urlToBuffer(o.challenge), user: Object.assign({}, o.user, { id: b64urlToBuffer(o.user.id) }), excludeCredentials: (o.excludeCredentials || []).map(function (c) { return Object.assign({}, c, { id: b64urlToBuffer(c.id) }); }), }); } function decodeAuthenticationOptions(o) { return Object.assign({}, o, { challenge: b64urlToBuffer(o.challenge), allowCredentials: (o.allowCredentials || []).map(function (c) { return Object.assign({}, c, { id: b64urlToBuffer(c.id) }); }), }); } // --- Credential encoding (ArrayBuffer fields → b64url for JSON) ------- function encodeRegistrationCredential(cred) { return { id: cred.id, rawId: bufferToB64url(cred.rawId), type: cred.type, response: { clientDataJSON: bufferToB64url(cred.response.clientDataJSON), attestationObject: bufferToB64url(cred.response.attestationObject), transports: typeof cred.response.getTransports === 'function' ? cred.response.getTransports() : [], }, clientExtensionResults: cred.getClientExtensionResults ? cred.getClientExtensionResults() : {}, }; } function encodeAuthenticationAssertion(cred) { return { id: cred.id, rawId: bufferToB64url(cred.rawId), type: cred.type, response: { clientDataJSON: bufferToB64url(cred.response.clientDataJSON), authenticatorData: bufferToB64url(cred.response.authenticatorData), signature: bufferToB64url(cred.response.signature), userHandle: cred.response.userHandle ? bufferToB64url(cred.response.userHandle) : null, }, clientExtensionResults: cred.getClientExtensionResults ? cred.getClientExtensionResults() : {}, }; } // --- HTTP helper ------------------------------------------------------ async function postJson(url, body, csrfToken) { const headers = { 'Content-Type': 'application/json' }; if (csrfToken) headers['X-CSRFToken'] = csrfToken; const r = await fetch(url, { method: 'POST', headers: headers, body: JSON.stringify(body || {}), credentials: 'same-origin', }); let data = null; try { data = await r.json(); } catch (_) {} return { ok: r.ok, status: r.status, data: data }; } // --- Public API: wire enrolment button -------------------------------- function wireRegisterButton(cfg) { const btn = document.getElementById(cfg.buttonId); const labelEl = cfg.labelInputId ? document.getElementById(cfg.labelInputId) : null; const statusEl = cfg.statusElementId ? document.getElementById(cfg.statusElementId) : null; if (!btn) return; btn.addEventListener('click', async function () { if (statusEl) statusEl.textContent = 'Préparation...'; btn.disabled = true; try { if (!('credentials' in navigator) || !navigator.credentials.create) { throw new Error('Votre navigateur ne supporte pas les passkeys.'); } const beginRes = await postJson(cfg.beginUrl, {}, cfg.csrfToken); if (!beginRes.ok) { throw new Error((beginRes.data && beginRes.data.message) || 'Erreur de préparation'); } const opts = decodeRegistrationOptions(beginRes.data); if (statusEl) statusEl.textContent = 'Suivez les instructions de votre authentificateur...'; const cred = await navigator.credentials.create({ publicKey: opts }); const encoded = encodeRegistrationCredential(cred); const finishRes = await postJson( cfg.finishUrl, { response: encoded, label: labelEl ? labelEl.value : '' }, cfg.csrfToken ); if (!finishRes.ok) { throw new Error((finishRes.data && finishRes.data.message) || 'Échec de la vérification'); } if (statusEl) statusEl.textContent = 'Passkey enregistrée. Rafraîchissement...'; setTimeout(function () { window.location.reload(); }, 800); } catch (e) { if (statusEl) statusEl.textContent = 'Erreur : ' + (e.message || e); btn.disabled = false; } }); } // --- Public API: wire login button ------------------------------------ function wireAuthButton(cfg) { const btn = document.getElementById(cfg.buttonId); const statusEl = cfg.statusElementId ? document.getElementById(cfg.statusElementId) : null; if (!btn) return; btn.addEventListener('click', async function () { if (statusEl) statusEl.textContent = 'Préparation...'; btn.disabled = true; try { if (!('credentials' in navigator) || !navigator.credentials.get) { throw new Error('Votre navigateur ne supporte pas les passkeys.'); } const beginRes = await postJson(cfg.beginUrl, {}, cfg.csrfToken); if (!beginRes.ok) { throw new Error((beginRes.data && beginRes.data.message) || 'Erreur de préparation'); } const opts = decodeAuthenticationOptions(beginRes.data); if (statusEl) statusEl.textContent = 'Confirmez avec votre authentificateur...'; const cred = await navigator.credentials.get({ publicKey: opts }); const encoded = encodeAuthenticationAssertion(cred); const finishRes = await postJson(cfg.finishUrl, { response: encoded }, cfg.csrfToken); if (!finishRes.ok) { throw new Error((finishRes.data && finishRes.data.message) || 'Échec de la vérification'); } window.location.assign((finishRes.data && finishRes.data.redirect) || '/'); } catch (e) { if (statusEl) statusEl.textContent = 'Erreur : ' + (e.message || e); btn.disabled = false; } }); } global.DictIAWebAuthn = { wireRegisterButton: wireRegisterButton, wireAuthButton: wireAuthButton, }; })(window);