| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
-
- 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();
|