javascriptgoogle-chromeauthenticationwebauthnfido

FIDO2 seamless sign-in across devices - Passkeys


EDIT 2

Just to ellaborate on what @Tim has already explained, I made these changes to the credential.create Authenticator Selection Options: -

    authenticatorSelection: {
        //This defaults to false but I specified it anyway
        requireResidentKey: false,
        //This defaults to "preferred" and gives a warning in Chrome if not specified.
        //Not sure if it has any functional impact
        userVerification: "discouraged",
        //This was required to get it to work
        authenticatorAttachment: "cross-platform" 
    },

Apart from the first time it still prompting for USB dongle, it works like a dream!

It even turns on BlueTooth if you've forgotten. Wow! (How does it know my phone in this case? 'Cos I'm logged on with the same Chrome account PC & Phone? Some registry?)

Anyway this functionality is just the mutt's nuts! I have tried to drag sites in Perth Western Australia kicking and screaming to FIDO2 but this has to be the clincher.

Well done to all involved!

EDIT 2 END

Edit Start

If you are just trying to test the new FIDO cross device authentication flow using your phone,

Yes that is exactly what I'm trying to achieve. I have now rolled back to just: -

  1. Bluetooth enabled on my Android phone
  2. Bluetooth visible
  3. Logged in to Google account both PC/Chrome and Phone
  4. Windows hello PIN set for my windows account

But all the bellow code gives me is the option to enter my PIN no "Add a new Android phone"

"You are mixing a few different things here. The security key, local platform authenticator (Windows Hello) and your phone will all have their own credential."

I'm sure you're correct. I was just pulling all the leavers I knew trying to make it work:- Windows Phone Link, Acccount.Live.Com account options, etc

do not require a resident credential (not yet supported) and do not set an attachment preference.

Not sure what that means. If you mean the USB key then fine I don't want to use it, but I was getting prompted for it. (See below)

When prompted in Chrome, add your phone to link it, scan the QR on your phone and then perform the UV gesture.

Ok, what QR code reader are you using?

My problem is Chrome is not prompting me to add a phone :-( There's a configuration or API argument I'm missing?

Please help.

Edit End

I understand the "available for developers later this year" caveat, but as a FIDO devotee I have been very excited about the previewed functionality from Passkeys 9:00+. My appetite has been further whetted by the latent support in the current version of Chrome/Samsung/Windows!

TL;DR

  1. I have paired my Samsung/Android phone with my Windows PC
  2. I have enabled my Yubikey USB device (without Yubikey software)
  3. I call navigator.credentials.create and Windows prompts me for my PIN
  4. I then call navigator.credentials.get and Windows tells me that it doesn't recognize my YubiKey Bad USB
  5. On Cancel, I am then prompted for an alternative device device
  6. If I choose my SM_* phone my phone prompts me for my finger print and Samsung/Android is happy!
  7. Chrome is NOT NOT happy

So while I willing acknowledge/concede this is "emerging" technology: -

  1. Why does Create prompt for a PIN and not a fingerprint
  2. I've tried setting various Google account options and Windows Live options and Windows/Accounts options; which are in play?
  3. My phone is paired with Windows/Bluetooth; is this not enough for Chrome?
  4. If I choose "Add a new Android phone" I get the dinosaur QRCode. On my phone Samsung browser is my only QRC reader which returns FIDO:/484543913687778941263973123987003762051850670080716404329165 . . . Chrome does not recognize it; where does it go?
  5. Here is Android/Samsumg prompting me for my bio-metric

Please see source code below.

const utf8Decoder = new TextDecoder('utf-8');

async function verifyCredential() {
    var keyResult = await getKey();
    var serverChallenge = JSON.parse(keyResult);
    var credentialId = localStorage.getItem("credentialId");
    if (!credentialId) {
        throw new Error("You must create a Credential first");
    }

    var allowCredentials = [{
        type: "public-key",
        id: Uint8Array.from(atob(credentialId), x => x.charCodeAt(0)).buffer
    }]

    var getAssertionOptions = {
        timeout: 30000,
        challenge: Uint8Array.from(serverChallenge.Token, c => c.charCodeAt(0)).buffer,
        allowCredentials: allowCredentials,
        userVerification: "required"
    };

    return navigator.credentials.get({
        publicKey: getAssertionOptions
    }).then(rawAssertion => {
        var assertion = {
            id: base64encode(rawAssertion.rawId),
            clientDataJSON: utf8Decoder.decode(rawAssertion.response.clientDataJSON),
            userHandle: base64encode(rawAssertion.response.userHandle),
            signature: base64encode(rawAssertion.response.signature),
            authenticatorData: base64encode(rawAssertion.response.authenticatorData)
        };

        // Check id = allowcredentials.id
        console.log("=== Assertion response ===");
        console.log(assertion);
        verifyAssertion(assertion).then(
            result => {
                var res = JSON.parse(result);
                console.log(res.success);
                if (res.success) {
                }
            });

        return;

    }).catch(
        (err) => {
            if (err.name == "NotAllowedError") {
                console.log("here " + err.name);
            } else {
                console.log("other " + err.name);
            }
            return Promise.resolve(false);
        });
}

async function createCredential() {
    var keyResult = await getKey();
    var serverChallenge = JSON.parse(keyResult);

    var createCredentialOptions = {
        rp: {
            name: "WebAuthn Sample App",
            icon: ""
        },
        user: {
            id: Uint8Array.from("some.user.guid", c => c.charCodeAt(0)),
            name: "maherrj@gmail.com",
            displayName: "Richard Maher",
            icon: ""
        },
        pubKeyCredParams: [
            {
                //External authenticators support the ES256 algorithm
                type: "public-key",
                alg: -7
            },
            {
                //Windows Hello supports the RS256 algorithm
                type: "public-key",
                alg: -257
            }
        ],
        authenticatorSelection: {
            //Select authenticators that support username-less flows
            //requireResidentKey: true,

            //Select authenticators that have a second factor (e.g. PIN, Bio) "preferred" "discouraged"
            userVerification: "required",
            //Selects between bound or detachable authenticators
            authenticatorAttachment: "platform"  // Optional
        },
        //Since Edge shows UI, it is better to select larger timeout values
        timeout: 30000,
        //an opaque challenge that the authenticator signs over
        challenge: Uint8Array.from(serverChallenge.Token, c => c.charCodeAt(0)).buffer,
        //prevent re-registration by specifying existing credentials here
        excludeCredentials: [],
        //specifies whether you need an attestation statement
        attestation: "none"
    };

    const authAbort = new AbortController();
    const abortSignal = authAbort.signal;
    abortSignal.addEventListener("abort", (e) => { console.log("It has been aborted"); });

    return navigator.credentials.create({
        publicKey: createCredentialOptions,
        signal: abortSignal
    }).then(rawAttestation => {
        var attestation = {
            id: base64encode(rawAttestation.rawId),
            clientDataJSON: utf8Decoder.decode(rawAttestation.response.clientDataJSON),
            attestationObject: base64encode(rawAttestation.response.attestationObject)
        };

        console.log("=== Attestation response ===");
        console.log(attestation);
        verifyCredentials(attestation).then(
            result => {
                var res = JSON.parse(result);
                console.log(res.success);
                if (res.success) {
                    localStorage.setItem("credentialId", res.id);
                }
            });

        return;

    }).catch(
        (err) => {
            if (err.name == "NotAllowedError") {
                console.log("here " + err.name);
            } else {
                console.log("other " + err.name);
            }
            return Promise.resolve(false);
        });
}

async function verifyCredentials(attestation) {
    let params = JSON.stringify(attestation);
    let resp = await fetch("api/fido/verifycredentials", {
        method: "POST",
        headers: { "Content-type": "application/json", "Accept": "application/json" },
        body: params
    });

    var myStat;
    if (resp.ok) {
        myStat = await resp.json();
        console.log("Stat vc = " + myStat)
    } else {
        console.log("boom");
    }
    console.log("done ");
    return myStat;
}

async function verifyAssertion(assertion) {
    let params = JSON.stringify(assertion);
    let resp = await fetch("api/fido/verifyassertion", {
        method: "POST",
        headers: { "Content-type": "application/json", "Accept": "application/json" },
        body: params
    });

    var myStat;
    if (resp.ok) {
        myStat = await resp.json();
        console.log("Stat va = " + myStat)
    } else {
        console.log("boom");
    }
    console.log("done ");
    return myStat;
}

async function getKey() {
    let resp = await fetch("api/fido/getkey", {
        method: "GET",
        headers: { "Content-type": "application/json", "Accept": "application/json" }
    });

    var mykey;
    if (resp.ok) {
        mykey = await resp.json();
        console.log("key = " + mykey)
    } else {
        throw new Error("boom");
    }
    console.log("done key");
    return mykey;
}

function base64encode(arrayBuffer) {
    if (!arrayBuffer || arrayBuffer.length == 0)
        return undefined;

    return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

Solution

  • You are mixing a few different things here. The security key, local platform authenticator (Windows Hello) and your phone will all have their own credential.

    If you are just trying to test the new FIDO cross device authentication flow using your phone, do not require a resident credential (not yet supported) and do not set an attachment preference.

    When prompted in Chrome, add your phone to link it, scan the QR on your phone and then perform the UV gesture.