PageSourceSearch

https://meetyou.ai/assets/js/webauthn.js

js meetyou.ai collected 2026-10-05 14:24:00 UTC 12,781 bytes, 370 lines download raw bytes

1/**
2 * MIT License
3 *
4 * Copyright (c) Italo Israel Baeza Cabrera
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25class WebAuthn {
26    /**
27     * Routes for WebAuthn assertion (login) and attestation (register).
28     *
29     * @type {{registerOptions: string, register: string, loginOptions: string, login: string, }}
30     */
31    #routes = {
32        registerOptions: "webauthn/register/options",
33        register: "webauthn/register",
34        loginOptions: "webauthn/login/options",
35        login: "webauthn/login",
36    }
37
38    /**
39     * Headers to use in ALL requests done.
40     *
41     * @type {{Accept: string, "Content-Type": string, "X-Requested-With": string}}
42     */
43    #headers = {
44        "Accept": "application/json",
45        "Content-Type": "application/json",
46        "X-Requested-With": "XMLHttpRequest"
47    };
48
49    /**
50     * If set to true, the credentials option will be set to 'include' on all fetch calls,
51     * or else it will use the default 'same-origin'. Use this if the backend is not the
52     * same origin as the client or the XSRF protection will break without the session.
53     *
54     * @type {boolean}
55     */
56    #includeCredentials = false
57
58    /**
59     * Create a new WebAuthn instance.
60     *
61     * @param routes {{registerOptions: string, register: string, loginOptions: string, login: string}}
62     * @param headers {{string}}
63     * @param includeCredentials {boolean}
64     * @param xcsrfToken {string|null} Either a csrf token (40 chars) or xsrfToken (224 chars)
65     */
66    constructor(routes = {}, headers = {}, includeCredentials = false, xcsrfToken = null) {
67        Object.assign(this.#routes, routes);
68        Object.assign(this.#headers, headers);
69
70        this.#includeCredentials = includeCredentials;
71
72        let xsrfToken;
73        let csrfToken;
74
75        if (xcsrfToken === null) {
76            // If the developer didn't issue an XSRF token, we will find it ourselves.
77            xsrfToken = WebAuthn.#XsrfToken;
78            csrfToken = WebAuthn.#firstInputWithCsrfToken;
79        } else{
80            // Check if it is a CSRF or XSRF token
81            if (xcsrfToken.length === 40) {
82                csrfToken = xcsrfToken;
83            } else if (xcsrfToken.length === 224) {
84                xsrfToken = xcsrfToken;
85            } else {
86                throw new TypeError('CSRF token or XSRF token provided does not match requirements. Must be 40 or 224 characters.');
87            }
88        }
89
90        if (xsrfToken !== null) {
91            this.#headers["X-XSRF-TOKEN"] ??= xsrfToken;
92        } else if (csrfToken !== null) {
93            this.#headers["X-CSRF-TOKEN"] ??= csrfToken;
94        } else {
95            // We didn't find it, and since is required, we will bail out.
96            throw new TypeError('Ensure a CSRF/XSRF token is manually set, or provided in a cookie "XSRF-TOKEN" or or there is meta tag named "csrf-token".');
97        }
98    }
99
100    /**
101     * Returns the CSRF token if it exists as a form input tag.
102     *
103     * @returns string
104     * @throws TypeError
105     */
106    static get #firstInputWithCsrfToken() {
107        // First, try finding an CSRF Token in the head.
108        let token = Array.from(document.head.getElementsByTagName("meta"))
109            .find(element => element.name === "csrf-token");
110
111        if (token) {
112            return token.content;
113        }
114
115        // Then, try to find a hidden input containing the CSRF token.
116        token = Array.from(document.getElementsByTagName('input'))
117            .find(input => input.name === "_token" && input.type === "hidden")
118
119        if (token) {
120            return token.value;
121        }
122
123        return null;
124    }
125
126    /**
127     * Returns the value of the XSRF token if it exists in a cookie.
128     *
129     * Inspired by https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#example_2_get_a_sample_cookie_named_test2
130     *
131     * @returns {?string}
132     */
133     static get #XsrfToken() {
134        const cookie = document.cookie.split(";").find((row) => /^\s*(X-)?[XC]SRF-TOKEN\s*=/.test(row));
135        // We must remove all '%3D' from the end of the string.
136        // Background:
137        // The actual binary value of the CSFR value is encoded in Base64.
138        // If the length of original, binary value is not a multiple of 3 bytes,
139        // the encoding gets padded with `=` on the right; i.e. there might be
140        // zero, one or two `=` at the end of the encoded value.
141        // If the value is sent from the server to the client as part of a cookie,
142        // the `=` character is URL-encoded as `%3D`, because `=` is already used
143        // to separate a cookie key from its value.
144        // When we send back the value to the server as part of an AJAX request,
145        // Laravel expects an unpadded value.
146        // Hence, we must remove the `%3D`.
147        return cookie ? cookie.split("=")[1].trim().replaceAll("%3D", "") : null;
148    };
149
150    /**
151     * Returns a fetch promise to resolve later.
152     *
153     * @param data {Object}
154     * @param route {string}
155     * @param headers {{string}}
156     * @returns {Promise<Response>}
157     */
158    #fetch(data, route, headers = {}) {
159        const url = new URL(route, window.location.origin).href;
160        
161        return fetch(url, {
162            method: "POST",
163            credentials: this.#includeCredentials ? "include" : "same-origin",
164            redirect: "error",
165            headers: {...this.#headers, ...headers},
166            body: JSON.stringify(data)
167        });
168    }
169
170    /**
171     * Decodes a BASE64 URL string into a normal string.
172     *
173     * @param input {string}
174     * @returns {string|Iterable}
175     */
176    static #base64UrlDecode(input) {
177        input = input.replace(/-/g, "+").replace(/_/g, "/");
178
179        const pad = input.length % 4;
180
181        if (pad) {
182            if (pad === 1) {
183                throw new Error("InvalidLengthError: Input base64url string is the wrong length to determine padding");
184            }
185
186            input += new Array(5 - pad).join("=");
187        }
188
189        return atob(input);
190    }
191
192    /**
193     * Transform a string into Uint8Array instance.
194     *
195     * @param input {string}
196     * @param useAtob {boolean}
197     * @returns {Uint8Array}
198     */
199    static #uint8Array(input, useAtob = false) {
200        return Uint8Array.from(
201            useAtob ? atob(input) : WebAuthn.#base64UrlDecode(input), c => c.charCodeAt(0)
202        );
203    }
204
205    /**
206     * Encodes an array of bytes to a BASE64 URL string
207     *
208     * @param arrayBuffer {ArrayBuffer|Uint8Array}
209     * @returns {string}
210     */
211    static #arrayToBase64String(arrayBuffer) {
212        return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
213    }
214
215    /**
216     * Parses the Public Key Options received from the Server for the browser.
217     *
218     * @param publicKey {Object}
219     * @returns {Object}
220     */
221    #parseIncomingServerOptions(publicKey) {
222        console.debug(publicKey);
223
224        publicKey.challenge = WebAuthn.#uint8Array(publicKey.challenge);
225
226        if ('user' in publicKey) {
227            publicKey.user = {
228                ...publicKey.user,
229                id: WebAuthn.#uint8Array(publicKey.user.id)
230            };
231        }
232
233        [
234            "excludeCredentials",
235            "allowCredentials"
236        ]
237            .filter(key => key in publicKey)
238            .forEach(key => {
239                publicKey[key] = publicKey[key].map(data => {
240                    return {...data, id: WebAuthn.#uint8Array(data.id)};
241                });
242            });
243
244        console.log(publicKey);
245
246        return publicKey;
247    }
248
249    /**
250     * Parses the outgoing credentials from the browser to the server.
251     *
252     * @param credentials {Credential|PublicKeyCredential}
253     * @return {{response: {string}, rawId: string, id: string, type: string}}
254     */
255    #parseOutgoingCredentials(credentials) {
256        let parseCredentials = {
257            id: credentials.id,
258            type: credentials.type,
259            rawId: WebAuthn.#arrayToBase64String(credentials.rawId),
260            response: {}
261        };
262
263        [
264            "clientDataJSON",
265            "attestationObject",
266            "authenticatorData",
267            "signature",
268            "userHandle"
269        ]
270            .filter(key => key in credentials.response)
271            .forEach(key => parseCredentials.response[key] = WebAuthn.#arrayToBase64String(credentials.response[key]));
272
273        return parseCredentials;
274    }
275
276    /**
277     * Handles the response from the Server.
278     *
279     * Throws the entire response if is not OK (HTTP 2XX).
280     *
281     * @param response {Response}
282     * @returns Promise<JSON|ReadableStream>
283     * @throws Response
284     */
285    static #handleResponse(response) {
286        if (!response.ok) {
287            throw response;
288        }
289
290        // Here we will do a small trick. Since most of the responses from the server
291        // are JSON, we will automatically parse the JSON body from the response. If
292        // it's not JSON, we will push the body verbatim and let the dev handle it.
293        return new Promise((resolve) => {
294            response
295                .json()
296                .then((json) => resolve(json))
297                .catch(() => resolve(response.body));
298        });
299    }
300
301    /**
302     * Register the user credentials from the browser/device.
303     *
304     * You can add request input if you are planning to register a user with WebAuthn from scratch.
305     *
306     * @param request {{string}}
307     * @param response {{string}}
308     * @returns Promise<JSON|ReadableStream>
309     */
310    async register(request = {}, response = {}) {
311        const optionsResponse = await this.#fetch(request, this.#routes.registerOptions);
312        const json = await optionsResponse.json();
313        const publicKey = this.#parseIncomingServerOptions(json);
314        const credentials = await navigator.credentials.create({publicKey});
315        const publicKeyCredential = this.#parseOutgoingCredentials(credentials);
316
317        Object.assign(publicKeyCredential, response);
318        Object.assign(publicKeyCredential, request);
319
320        return await this.#fetch(publicKeyCredential, this.#routes.register).then(WebAuthn.#handleResponse);
321    }
322
323    /**
324     * Log in a user with his credentials.
325     *
326     * If no credentials are given, the app may return a blank assertion for userless login.
327     *
328     * @param request {{string}}
329     * @param response {{string}}
330     * @returns Promise<JSON|ReadableStream>
331     */
332    async login(request = {}, response = {}) {
333        const optionsResponse = await this.#fetch(request, this.#routes.loginOptions);
334        const json = await optionsResponse.json();
335        const publicKey = this.#parseIncomingServerOptions(json);
336        const credentials = await navigator.credentials.get({publicKey})
337          .catch((err) => {
338            if (err.name === 'NotAllowedError') {
339              document.getElementById("alert").innerHTML = 'This device has not a MeetYou registered account.';
340            } else {
341              
342            }
343          });
344        const publicKeyCredential = this.#parseOutgoingCredentials(credentials);
345
346        Object.assign(publicKeyCredential, response);
347
348        console.log(publicKeyCredential);
349
350        return await this.#fetch(publicKeyCredential, this.#routes.login, response).then(WebAuthn.#handleResponse);
351    }
352
353    /**
354     * Checks if the browser supports WebAuthn.
355     *
356     * @returns {boolean}
357     */
358    static supportsWebAuthn() {
359        return typeof PublicKeyCredential != "undefined";
360    }
361
362    /**
363     * Checks if the browser doesn't support WebAuthn.
364     *
365     * @returns {boolean}
366     */
367    static doesntSupportWebAuthn() {
368        return !this.supportsWebAuthn();
369    }
370}

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.