function base64UrlEncode(value) { const encoded = btoa(value); return encoded .replace(/\+/g, "-") // '+' => '-' .replace(/\//g, "_") // '/' => '_' .replace(/=*$/g, ""); // trim trailing '=' padding } function base64UrlDecode(value) { const base64 = value .replace(/-/g, "+") .replace(/_/g, "/"); return atob(base64); } function coerceToArrayBuffer(value) { if (!(typeof value === "string")) { throw new Error(`Invalid input type. Expected string but got ${typeof(value)}`); } // base64url to base64 const str = base64UrlDecode(value); // base64 to Uint8Array const bytes = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { bytes[i] = str.charCodeAt(i); } return bytes; }; function coerceToBase64Url(value) { if (!(value instanceof Uint8Array)) { throw new Error(`Invalid input type. Expected Uint8Array but got ${typeof(value)}`); } let str = ""; for (let i = 0; i < value.byteLength; i++) { str += String.fromCharCode(value[i]); } return window.btoa(str); } class Registration { constructor() { document.getElementById('start').addEventListener('click', this.registerAccount.bind(this)); } async registerAccount(event) { event.preventDefault(); const username = 'Foo! That Bar'; const login = base64UrlEncode(username); let options; try { options = await this.buildCredentialOptions(login); } catch (e) { console.error("Building create options failed with exception", e); return; } // Create the Credential with navigator.credentials.create API let newCredential; try { newCredential = await navigator.credentials.create({ publicKey: options }); } catch (e) { console.error("credentials.create failed with exception", e); return; } console.log("PublicKeyCredential created", newCredential); let result; try { result = this.register(login, newCredential); } catch (e) { console.error("Registering new credentials with the server failed with exception", e); return; } console.log("New login created", result); } async buildCredentialOptions(login) { const response = await fetch(`/buildCredentialOptions?login=${login}`, { method: 'GET', headers: { 'Accept': 'application/json' } }); const options = await response.json(); if (options.status !== "ok") { throw new Error(`Error in buildCredentialOptions: ${options.errorMessage}`); } console.log("Got options", options); // Turn the challenge back into the accepted format of padded base64 options.challenge = coerceToArrayBuffer(options.challenge); // Turn ID into a UInt8Array Buffer for some reason options.user.id = coerceToArrayBuffer(options.user.id); options.excludeCredentials = options.excludeCredentials.map((c) => { c.id = coerceToArrayBuffer(c.id); return c; }); console.log("Options Formatted", options); return options; } async register(login, newCredential) { // Move data into Arrays in case it is super long // TODO: check! let attestationObject = new Uint8Array(newCredential.response.attestationObject); let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); let rawId = new Uint8Array(newCredential.rawId); const data = { id: newCredential.id, rawId: coerceToBase64Url(rawId), type: newCredential.type, extensions: newCredential.getClientExtensionResults(), response: { AttestationObject: coerceToBase64Url(attestationObject), clientDataJSON: coerceToBase64Url(clientDataJSON), transports: newCredential.response.getTransports() } }; let response = await fetch(`/registerCredential?login=${login}`, { method: 'POST', body: JSON.stringify(data), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); const result = await response.json(); console.log("Credential Object", result); if (result.status !== "ok") { throw new Error(`Error creating credential: ${result.errorMessage}`); } return result; } } window.registration = new Registration();