/** * STAIRCASE WEBAUTHN CLIENT * * Handles WebAuthn (Passkey) operations: * - Registration (create new passkey) * - Authentication (login with passkey) * * @file sc_webauthn.js * @version 1.0.0 * @date 2025-01-08 */ // Use window.ScWebAuthn to avoid "already declared" errors when script loads multiple times window.ScWebAuthn = window.ScWebAuthn || { /** * Check if WebAuthn is supported by browser */ isSupported: function() { return window.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function'; }, /** * Convert Base64 URL to ArrayBuffer */ base64urlToBuffer: function(base64url) { const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/'); const padLen = (4 - (base64.length % 4)) % 4; const padded = base64 + '='.repeat(padLen); const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; }, /** * Convert ArrayBuffer to Base64 URL */ bufferToBase64url: function(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } const base64 = btoa(binary); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }, /** * PASSKEY REGISTRATION * Creates a new passkey for the current user */ register: function(deviceName, successCallback, errorCallback) { if (!this.isSupported()) { errorCallback('Ihr Browser unterstützt keine Passkeys. Bitte verwenden Sie einen modernen Browser.'); return; } // Request challenge from server $.ajax({ url: 'rpc/ajax.php', type: 'GET', data: { type: 'passkey', action: 'passkey_challenge_register', csrf_token: $('#csrf_token').val() }, success: function(response) { if (!response.success) { errorCallback(response.error || 'Challenge-Abruf fehlgeschlagen'); return; } const options = response.data; // Convert Base64 strings to ArrayBuffers options.challenge = ScWebAuthn.base64urlToBuffer(options.challenge); options.user.id = ScWebAuthn.base64urlToBuffer(options.user.id); // Create credential navigator.credentials.create({ publicKey: options }) .then(function(credential) { // Prepare credential data for server const credentialData = { id: credential.id, rawId: ScWebAuthn.bufferToBase64url(credential.rawId), type: credential.type, response: { clientDataJSON: ScWebAuthn.bufferToBase64url(credential.response.clientDataJSON), attestationObject: ScWebAuthn.bufferToBase64url(credential.response.attestationObject) }, device_name: deviceName }; // Send to server for verification $.ajax({ url: 'rpc/ajax.php', type: 'POST', contentType: 'application/json', data: JSON.stringify({ type: 'passkey', action: 'passkey_register', ...credentialData }), success: function(verifyResponse) { if (verifyResponse.success) { successCallback(verifyResponse.data); } else { errorCallback(verifyResponse.error || 'Passkey-Registrierung fehlgeschlagen'); } }, error: function() { errorCallback('Server-Fehler bei Passkey-Verifizierung'); } }); }) .catch(function(error) { console.error('WebAuthn create error:', error); if (error.name === 'NotAllowedError') { errorCallback('Passkey-Erstellung abgebrochen'); } else { errorCallback('Passkey konnte nicht erstellt werden: ' + error.message); } }); }, error: function() { errorCallback('Server-Fehler beim Challenge-Abruf'); } }); }, /** * PASSKEY AUTHENTICATION * Authenticates user with existing passkey (USERLESS) */ authenticate: function(successCallback, errorCallback) { if (!this.isSupported()) { errorCallback('Ihr Browser unterstützt keine Passkeys.'); return; } // Request challenge from server (OHNE Username!) $.ajax({ url: 'rpc/ajax.php', type: 'GET', data: { type: 'passkey', action: 'passkey_challenge_auth', csrf_token: $('#csrf_token').val() }, success: function(response) { if (!response.success) { errorCallback(response.error || 'Challenge-Abruf fehlgeschlagen'); return; } const options = response.data; // Convert Base64 strings to ArrayBuffers options.challenge = ScWebAuthn.base64urlToBuffer(options.challenge); if (options.allowCredentials) { options.allowCredentials = options.allowCredentials.map(function(cred) { return { ...cred, id: ScWebAuthn.base64urlToBuffer(cred.id) }; }); } // Get credential navigator.credentials.get({ publicKey: options }) .then(function(assertion) { // Prepare assertion data for server const assertionData = { id: assertion.id, rawId: ScWebAuthn.bufferToBase64url(assertion.rawId), type: assertion.type, response: { clientDataJSON: ScWebAuthn.bufferToBase64url(assertion.response.clientDataJSON), authenticatorData: ScWebAuthn.bufferToBase64url(assertion.response.authenticatorData), signature: ScWebAuthn.bufferToBase64url(assertion.response.signature), userHandle: assertion.response.userHandle ? ScWebAuthn.bufferToBase64url(assertion.response.userHandle) : null } }; successCallback(assertionData); }) .catch(function(error) { console.error('WebAuthn get error:', error); if (error.name === 'NotAllowedError') { errorCallback('Passkey-Authentifizierung abgebrochen'); } else { errorCallback('Passkey-Authentifizierung fehlgeschlagen: ' + error.message); } }); }, error: function() { errorCallback('Server-Fehler beim Challenge-Abruf'); } }); } };