1// https://fusionauth.io/docs/lifecycle/authenticate-users/passwordless/webauthn-passkeys 2class FusionAuthWebAuthn { 3 extraPayloadData = {}; 4 5 async startRegistration() { 6 const response = await this.executeHttpRequest('createPasskeyStart'); 7 const requestCredentialOptions = { 8 publicKey : { 9 ...response.data.options, 10 challenge : this.base64UrlToBuffer(response.data.options.challenge), 11 excludeCredentials : response.data.options.excludeCredentials?.map(credential => { 12 return { 13 ...credential, 14 id : this.base64UrlToBuffer(credential.id) 15 } 16 }) ?? [], 17 user : { 18 ...response.data.options.user, 19 id : this.base64UrlToBuffer(response.data.options.user.id) 20 } 21 } 22 }; 23 24 // Remove the authenticatorSelection key so that other authentication options will be available. 25 delete requestCredentialOptions.publicKey.authenticatorSelection; 26 27 return await navigator.credentials.create(requestCredentialOptions); 28 } 29 30 async completeRegistration(publicKeyCredential) { 31 const transformedCredentials = { 32 id : publicKeyCredential.id, 33 response : { 34 attestationObject : this.bufferToBase64Url(publicKeyCredential.response.attestationObject), 35 clientDataJSON : this.bufferToBase64Url(publicKeyCredential.response.clientDataJSON) 36 }, 37 transports : (typeof publicKeyCredential.response.getTransports === 'undefined') ? [] : publicKeyCredential.response.getTransports(), 38 type : publicKeyCredential.type 39 }; 40 41 return await this.executeHttpRequest('createPasskeyComplete', { 42 credential : transformedCredentials 43 }); 44 } 45 46 async startPasskeyAssertion() { 47 const response = await this.executeHttpRequest('startPasskeyAssertion'); 48 if (response.result === false) { 49 return response; 50 } 51 52 const requestCredentialOptions = { 53 publicKey : { 54 ...response.data.options, 55 challenge : this.base64UrlToBuffer(response.data.options.challenge), 56 allowCredentials : response.data.options.allowCredentials?.map(credential => { 57 return { 58 ...credential, 59 id : this.base64UrlToBuffer(credential.id) 60 } 61 }) ?? [] 62 } 63 }; 64 65 return await navigator.credentials.get(requestCredentialOptions); 66 } 67 68 async preparePasskeyAssertionCompletionPayload(publicKeyCredential) { 69 return { 70 id : publicKeyCredential.id, 71 response : { 72 authenticatorData : this.bufferToBase64Url(publicKeyCredential.response.authenticatorData), 73 clientDataJSON : this.bufferToBase64Url(publicKeyCredential.response.clientDataJSON), 74 signature : this.bufferToBase64Url(publicKeyCredential.response.signature), 75 userHandle : this.bufferToBase64Url(publicKeyCredential.response.userHandle) ?? undefined 76 }, 77 type : publicKeyCredential.type, 78 clientExtensionResults : publicKeyCredential.getClientExtensionResults() 79 }; 80 } 81 82 setExtraPayloadData(extraPayloadData) { 83 this.extraPayloadData = extraPayloadData; 84 } 85 86 async executeHttpRequest(method, requestParams = {}) { 87 return await $.ajax({ 88 url : '/?' + $.param({ 89 option : 'com_ajax', 90 plugin : method, // ajaxfusionauth 91 format : 'json' 92 }), 93 method : 'POST', 94 data : { 95 ...requestParams, 96 ...this.extraPayloadData 97 }, 98 dataType : 'json' 99 }) 100 .then((response) => response.data[response.data.length - 1]); 101 } 102 103 base64UrlToBuffer(base64Url) { 104 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 105 const padLen = (4 - (base64.length % 4)) % 4; 106 return Uint8Array.from(atob(base64.padEnd(base64.length + padLen, '=')), char => char.charCodeAt(0)); 107 } 108 109 bufferToBase64Url(buffer) { 110 let string = ''; 111 const bytes = new Uint8Array(buffer);
112 bytes.forEach(byte => string += String.fromCharCode(byte)); 113 return btoa(string).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 114 } 115}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.