javascriptwebauthn

Webauthn on localhost failed to pass navigator.credentials.get()


I am making a sample-app to test a working flow of fido2-server.

I face a ploblem that after registration completed(with Mac touchId or Yubikey 5 series) chrome sais there is no pass key that registered on the website.

Steps I already made were

  1. registration request (configure Fido2Lib) with user id and send back challenge
  2. receive registration options from server and exec navigator.credentials.create
  3. send credentials to server and save public key to user.
  4. send login request with user id and return fido2 assertionOptions that has allowCredentials.
  5. then try to navigator.credentials.get and have a pop up of "there is no key to this website"

I need to determine what is the cause of the problem. First I use two way of registration that platform(touch id) and cross-platform(yubikey) for authenticatorAttachment.

when touch id, it sais there is no key to the website(localhost) when yubikey, this security key is not registered to the website.

I think I made misstake of challenge or allowCredential to pass navigator.credentials.get

codes are too long so I put a part of important.

registerRequest

const fido2 = new Fido2Lib({
  timeout: 60000,
  rpId: "localhost",
  rpName: "localhost:3000",
  rpIcon: "https://example.com/icon.png",
  challengeSize: 32,
  attestation: "direct",
  authenticatorAttachment: "cross-platform",
  authenticatorRequireResidentKey: false,
  authenticatorUserVerification: "preferred"
});
  const user = users[userId] || {
    id: userId,
    name: username,
    displayName: username,
  };
  users[userId] = user;

  const options = await fido2.attestationOptions();
  options.user = user;
  options.challenge = base64url.encode(options.challenge);
  if (options.user && options.user.id) {
    options.user.id = base64url.encode(Buffer.from(options.user.id, 'utf-8'));
  }

registerResponse


    req.body.rawId = new Uint8Array(base64url.toBuffer(req.body.id)).buffer;

    const attestationResult = await fido2.attestationResult(req.body, {
      challenge: user.challenge,
      origin: "http://localhost:3000",
      factor: "either"
    });

    if (attestationResult.audit.complete) {
      const d = attestationResult.authnrData;

      user.authenticator = JSON.stringify({
        publickey : d.get('credentialPublicKeyPem'),
        counter : d.get('counter'),
        fmt: d.get('fmt'),
        credId : base64url.encode(d.get('credId')),
      });
      users[userId] = user;

loginResponse

    const user = users[userId];
    const authenticator = JSON.parse(user.authenticator);
    const assertionOptions = await fido2.assertionOptions();
    assertionOptions.challenge = base64url.encode(assertionOptions.challenge);
    let allowCredentials = [];
    allowCredentials.push({
      type: 'public-key',
      id: authenticator.credId,
      transports: ['usb', 'nfc', 'ble']
      // transports: ['internal']
    });
    assertionOptions.allowCredentials = allowCredentials;

client.js for register

            const options = response.data;

            options.challenge = Uint8Array.from(atob(options.challenge), (c) => c.charCodeAt(0));
            options.user.id = Uint8Array.from(atob(options.user.id), (c) => c.charCodeAt(0));

            const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;

            const res = publicKeyCredentialToJSON(credential.response);
// publicKeyCredentialToJSON is kind of base64encode
            if (credential) {
                const attestationResponse = {
                    id: credential.id,
                    rawId: Array.from(new Uint8Array(credential.rawId)),
                    type: credential.type,
                    response: res
                };

client.js for login


            const options = response.data;
            options.challenge = Uint8Array.from(base64url.decode(options.challenge)).buffer;
            options.allowCredentials = options.allowCredentials.map((c: any) => {
                c.id = Uint8Array.from(base64url.decode(c.id)).buffer;
                return c;
            });
            const assertion = await navigator.credentials.get({ publicKey: options }) as PublicKeyCredential;

The passkey of localhost is successfully saved on my Mac.

Let me know if there is any information missing to help me solve the problem. Or tell me where I'm going wrong in this.


Solution

  • Mostly likely the credential ID is incorrect, or double-encoded.

    In a Chromium-based browser, you should be able to open <browser-name>://device-log (e.g. chrome://device-log) and see the WebAuthn requests logged as JSON structures. Copy-paste those here and check that the credential ID in the get request looks sensible. Try base64url decoding it and check that the result is not another base64url string, as that suggests double-encoding.