Laragear / webpass

The most simple WebAuthn (Passkeys) helper for browsers.
https://github.com/sponsors/DarkGhostHunter
MIT License
8 stars 0 forks source link

Does it support browser autofill? #17

Closed lslqtz closed 5 months ago

lslqtz commented 5 months ago

Please check these requirements

Description

I plan to switch from Laragear/WebAuthn-JSHelper to Laragear/webpass later this year, but I've made some modifications to the former so that it can support the browser's autofill feature.

Some browsers (such as Safari) can use passkey through autofill, but this has a prerequisite: the client should get options and call navigator.credentials.get (mediation: conditional).

So I'm wondering if this package is able to support this? Or, are there plans to support this in the future?

Thanks!

Example screenshot from Matt's Headroom | WWDC22: Meet passkeys:

(Translated by Google)

Code sample

Some code snippets I use in Laragear/WebAuthn-JSHelper. The problem with this implementation is: it may timeout. But it is enough for me.

webauthn.js

    async login(request = {}, response = {}, resetCredentials = false) {
        if (this.doesntSupportWebAuthn) {
            alert('Your browser does not support this feature!');
            throw new Error('Not support');
        }
        if (this.publicKey === null) {
            await this.prepareLogin();
        }

        if (this.credentials === null || resetCredentials) {
            let publicKey = this.publicKey;
            this.credentials = await navigator.credentials.get({publicKey}).catch();
        }

        if (this.credentials !== null) {
            const publicKeyCredential = this.parseOutgoingCredentials(this.credentials);
            Object.assign(publicKeyCredential, response);
//          console.debug(publicKeyCredential);
            return await this.fetch(publicKeyCredential, this.routes.login, response).then(WebAuthn.handleResponse);
        }
        throw new Error('No credentials');
    }

    async prepareLogin(request = {}, response = {}) {
        if (this.doesntSupportWebAuthn) {
            return;
        }
        const optionsResponse = await this.fetch(request, this.routes.loginOptions);
        const json = await optionsResponse.json();
        this.publicKey = this.parseIncomingServerOptions(json);
    }

    async waitLogin(silent = false) {
        if (this.publicKey === null) {
            await this.prepareLogin();
        }
        if (!PublicKeyCredential.isConditionalMediationAvailable || !PublicKeyCredential.isConditionalMediationAvailable()) {
            return;
        }
        this.waitController = new AbortController();
        let credentials = await navigator.credentials.get({
            mediation: (silent ? 'silent' : 'conditional'),
            publicKey: this.publicKey,
            signal: this.waitController.signal
        }).then((credentials) => {
            if (credentials !== null) {
                this.credentials = credentials;
                this.login({}, {
                    remember: ((this.rememberEle !== null && this.rememberEle.checked) ? 'on' : null)
                }).then(function () {
                    location.href = '/';
                }).catch(this.errorHandler);
            }
        }).catch(error => {});
    }

login.js

var login_WebAuthn = null;
$(document).ready(function () {
    login_WebAuthn = new WebAuthn();
    login_WebAuthn.rememberEle = document.getElementById('remember');
    login_WebAuthn.errorHandler = function WebAuthnException() {
        alert('Error.');
        login_WebAuthn.waitLogin();
    };
    login_WebAuthn.waitLogin();
    loginButton = document.getElementById('loginWithPasskey-button');
    if (loginButton !== null) {
        loginButton.addEventListener('click', function (event) {
            event.preventDefault();
            if (login_WebAuthn.waitController !== null) {
                login_WebAuthn.waitController.abort();
            }
            login_WebAuthn.login({}, {
                remember: (login_WebAuthn.rememberEle !== null && login_WebAuthn.rememberEle.checked ? 'on' : null)
            }, true).then(function () {
                location.href = '/';
            }).catch(login_WebAuthn.errorHandler);
        });
    }
});
DarkGhostHunter commented 5 months ago

If you can tell me how it works, I'll happy to implement it in the next version.

From what I can gather, the assertion options is done preemptively once the page loads, and then when the user clicks on the login, let the Authenticator sign the challenge with the credentials from the menu.

Since this package is usually paired with Laragear/WebAuthn, the challenge should be cycled before the expiration time.

lslqtz commented 5 months ago

Yes, when I use Laragear/WebAuthn-JSHelper (Without modification), it will only initiate an option request to obtain the public key when the button is clicked, and at the same time call the navigator.credentials.get method to pop up a modal box for interaction and login.

However, the autofill function requires that an option request be initiated to obtain the public key after the page is loaded, and the navigator.credentials.get method is called "silently". Ideally, it also requires re-rotating the public key after a certain period of time.

The code (Modified Laragear/WebAuthn-JSHelper) posted above implements an automatic filling, but does not support loop replacement challenge.

In my previous testing, Safari seemed to show up with autofill options after calling navigator.credentials.get. I guess this is how the page silently tells the browser that it supports Passkey autofill, and then the browser's autofill appears?

Since the implementation of this package seems to use @simplewebauthn/browser instead, I'm not quite sure how I should implement it.

lslqtz commented 5 months ago

Oh, I'm sorry! It seems to be an already implemented feature! Since I just switched over, I assumed it wasn't implemented yet. I will try it later!

https://github.com/Laragear/webpass?tab=readme-ov-file#autofill-passkey

DarkGhostHunter commented 5 months ago

Lol I implemented and didn't remember