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('register').addEventListener('click', this.registerAccount.bind(this)); this.usernameInput = document.getElementById('username'); } async registerAccount(event) { if (this.usernameInput.value.length < 3) { console.error("Username must at least be 3 characters long"); return; } const login = base64UrlEncode(this.usernameInput.value); 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; } } class Login { constructor() { document.getElementById('login').addEventListener('click', this.login.bind(this)); this.usernameInput = document.getElementById('username'); } async login(event) { const login = base64UrlEncode(this.usernameInput.value); let assertionOptions; try { assertionOptions = await this.buildAssertionOptions(login); } catch (e) { console.error("Building assertion options failed with exception", e); return; } // ask browser for credentials (browser will ask connected authenticators) let credential; try { credential = await navigator.credentials.get({ publicKey: assertionOptions }) console.log("credentials.get result", credential); } catch (e) { console.error("credentials.get failed with exception", e); return; } let result; try { result = await this.verifyCredential(login, credential); } catch (e) { console.error("Verifying credentials with the server failed with exception", e); return; } console.log("New login created", result); } async buildAssertionOptions(login) { let assertionOptions; const response = await fetch(`/buildAssertionOptions?login=${login}`, { method: 'GET', headers: { 'Accept': 'application/json' } }); assertionOptions = await response.json(); console.log("Raw Assertion Options", assertionOptions); assertionOptions.challenge = coerceToArrayBuffer(assertionOptions.challenge); // fix escaping. Change this to coerce assertionOptions.allowCredentials.map((listItem) => { listItem.id = coerceToArrayBuffer(listItem.id); return listItem; }); console.log("Assertion Options", assertionOptions); return assertionOptions; } async verifyCredential(login, credential) { let authData = new Uint8Array(credential.response.authenticatorData); let clientDataJSON = new Uint8Array(credential.response.clientDataJSON); let rawId = new Uint8Array(credential.rawId); let sig = new Uint8Array(credential.response.signature); const data = { id: credential.id, rawId: coerceToBase64Url(rawId), type: credential.type, extensions: credential.getClientExtensionResults(), response: { authenticatorData: coerceToBase64Url(authData), clientDataJSON: coerceToBase64Url(clientDataJSON), signature: coerceToBase64Url(sig) } }; let response = await fetch("/verifyCredential", { method: 'POST', body: JSON.stringify(data), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); const result = await response.json(); console.log("Assertion Object", result); if (result.status !== "ok") { throw new Error(`Error in verifyCredential: ${result.errorMessage}`); } return result; } } window.registration = new Registration(); window.login = new Login();