1

I'm try to follow instructions at https://webauthn.guide to register biometric login for a user in my site.

The register Javascript function is this:

var randomStringFromServer = "123456";
async function CreateANew()
{
    var bz = Uint8Array.from(randomStringFromServer);
    const publicKeyCredentialCreationOptions = {
        challenge: bz,
    rp: {
        name: "mysite.COM",
        id: "mysite.com",
    },
    user: {
        id: Uint8Array.from(
            'some_user_name'),
        name: 'some_name',
        displayName: 'some_display_name',
    },
    pubKeyCredParams: [{alg: -7, type: "public-key"}],
    authenticatorSelection: {
        
    },
    timeout: 60000,
    attestation: "direct"
};
   const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
    });
    if (!credential)
        return;
    
    // decode the clientDataJSON into a utf-8 string
    const utf8Decoder = new TextDecoder('utf-8');
    const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON);
    // parse the string as an object
    const clientDataObj = JSON.parse(decodedClientData);
    
    const decodedAttestationObj = CBOR.decode(
        credential.response.attestationObject);


        const {authData} = decodedAttestationObj;

        // get the length of the credential ID
        const dataView = new DataView(
            new ArrayBuffer(2));
        const idLenBytes = authData.slice(53, 55);
        idLenBytes.forEach(
            (value, index) => dataView.setUint8(
                index, value));
        const credentialIdLength = dataView.getUint16();

        // get the credential ID
        const credentialId = authData.slice(
            55, 55 + credentialIdLength);

        // get the public key object
        const publicKeyBytes = authData.slice(
            55 + credentialIdLength);

        // the publicKeyBytes are encoded again as CBOR
        const publicKeyObject = CBOR.decode(
            publicKeyBytes.buffer);
                        

    let CID = credentialId;
    let PID = publicKeyBytes;
    console.log(credentialId);
    $.ajax({
        url: "bio.php",
        method: "POST",
        data: {"create": 2, "type": <?= $tyx ?>, "uid" : <?= $u ?>, "challenge": clientDataObj.challenge, "origin": clientDataObj.origin,"ctype": clientDataObj.type,"authData": decodedAttestationObj.authData,"fmt" : decodedAttestationObj.fmt, "credentialID" : CID, "publicKeyBytes" : PID },
        success: function (result) {
            $("#result").html(result);
            if (result.startsWith("OK"))
                window.location = "bio.php";
        }
    });

This works, I get back a credentialID and a publicKeyBytes array in my PHP database.

<?php

 $ar = serialize($_POST['credentialID']); // and then store $ar to database

When however I'm trying to authenticate the user:

// $or = PHP array with the credentialID taken from the database
// $or = unserialize(...);
async function Login()
    {
        var bz = Uint8Array.from(randomStringFromServer);
        let id2 = "<?= implode(",",$or) ?>";
        let id2a = id2.replace(/, +/g, ",").split(",").map(Number);
        let id3 =  Uint8Array.from(id2a);
        console.log(id3);

        const publicKeyCredentialRequestOptions = {
        challenge: bz,                
        allowCredentials: [{
            id: id3,
            type: 'public-key',
            
        }],
timeout: 60000,

}

    const credential = await navigator.credentials.get({
        publicKey: publicKeyCredentialRequestOptions
    });
}

This time I only get an 'insert your USB key' in Chrome. It seems not to recognize my credentialID.

enter image description here

What am I doing wrong?

1 Answer 1

3

Most likely the credential ID is getting corrupted somewhere along the way. Try logging to to the console just after extracting it:

console.log(btoa(String.fromCharCode.apply(null, new Uint8Array(CID)));

And again just before making the authentication call:

console.log(btoa(String.fromCharCode.apply(null, new Uint8Array(id3)));

They should be the same value.


As an aside:

user: { id: Uint8Array.from('some_user_name'),

The user.id should not include any identifiable information, so a user name is a bad value there. I recommend generating a per-user, 128-bit random value in your database to use as user IDs.

// get the public key object
const publicKeyBytes = authData.slice(55 + credentialIdLength);

Extensions may follow the public key and this would pick them up. That might be fine if your CBOR decoder ignores trailing data. (You can also get the public key by calling getPublicKey on the attestation response, and you don't have to deal with CBOR that way.)

Sign up to request clarification or add additional context in comments.

4 Comments

Yes, they are the same value :(
Just to make sure that I understand: When you create the credential, you're using Windows Hello and not a security key? The credential ID returned matches what you're passing into the assertion call on the same machine, but Hello isn't recognising it and thinks that it must be on a security key? If so, can you log the base64 of the whole authdata and give me that, plus the credential ID that you're getting from it?
getPublicKey function is good. It seems you know your stuff when it comes to webauthn. Do you know of a better tutorial than webauthn.guide or maybe want to write yourself? Their tutorial seems outdated/incomplete.
> Do you know of a better tutorial than webauthn.guide or maybe want to write yourself? I'm not claiming that it's "better" because I've not read webauthn.guide, but I have previously written imperialviolet.org/2022/09/22/passkeys.html

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.