MasterKale / SimpleWebAuthn

WebAuthn, Simplified. A collection of TypeScript-first libraries for simpler WebAuthn integration. Supports modern browsers, Node, Deno, and more.
https://simplewebauthn.dev
MIT License
1.62k stars 137 forks source link

Erroring during verifying of passkey #490

Closed vincent-thomas closed 11 months ago

vincent-thomas commented 11 months ago

Describe the issue

Reproduction Steps

On /api/webauthn/setup

  const url = new URL(APP_URL);

  const options = await generateAuthenticationOptions({
    rpID: url.hostname,
    timeout: 60000,
    userVerification: 'preferred',
  });
  // Only for testing
  cookies.set('challenge', options.challenge);

  return { options };

On Client

const { options } = await fetch('/api/webauthn/setup', {
      method: 'POST',
    }).then(async (v) => v.json());

    const response = await startAuthentication(options, true);

    const response2 = await fetch('/api/webauthn/verify', {
      method: 'POST',
      body: JSON.stringify(response),
    }).then((v) => v.json());

On /api/webauthn/verify

const challenge = cookies().get('challenge')?.value;
  if (challenge === undefined) {
    /* redacted */
    return;
  }
  const body_UNSAFE = await req.json();

  const account = await getAccount();

  if (account.length === 0 ?? account.length > 1) {
    /* Redacted */
    return;
 }

  console.log(body_UNSAFE);

  const url = new URL(APP_URL);

  const response = await verifyAuthenticationResponse({
    response: body_UNSAFE,
    expectedChallenge: challenge,
    expectedOrigin: url.origin,
    expectedRPID: url.hostname,
    authenticator: {
      credentialID: Uint8Array.from(
        account[0].device_id.split('').map((c) => c.charCodeAt(0)),
      ),
      credentialPublicKey: Buffer.from(body_UNSAFE.id, 'base64url'),
      counter: -1,
    },
  });

Error when verifyAuthenticationResponse(...)

⨯ Error: Indefinite length not supported for byte or text strings
    at read (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:292:31)
    at read (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:347:54)
    at checkedRead (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:207:22)
    at Encoder.decode (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:155:24)
    at Encoder.decodeMultiple (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:171:37)
    at Module.decodeFirst (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/helpers/iso/isoCBOR.js:30:29)
    at decodeCredentialPublicKey (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/helpers/decodeCredentialPublicKey.js:9:108)
    at verifySignature (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/helpers/verifySignature.js:24:113)
    at verifyAuthenticationResponse (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse.js:166:101)
    at async POST (webpack-internal:///(rsc)/./src/app/api/webauthn/verify/route.ts:63:22)
    at async C:\Users\vincent\P\c\things\node_modules\.pnpm\next@14.0.3_@babel+core@7.23.3_react-dom@18.2.0_react@18.2.0\node_modules\next\dist\compiled\next-server\app-route.runtime.dev.js:6:62609 {
  lastPosition: 0,
  values: undefined
}

Expected behavior

Code Samples + WebAuthn Options and Responses

Dependencies

SimpleWebAuthn Libraries

{
    "@simplewebauthn/browser": "^8.3.4",
    "@simplewebauthn/server": "^8.3.5",
}
MasterKale commented 11 months ago

This looks wrong, when you're calling verifyAuthenticationResponse():

credentialPublicKey: Buffer.from(body_UNSAFE.id, 'base64url')

You're passing in a credential ID when the method expects the public key you received during registration for that credential ID. Try fixing that value when you call this method and it should solve the problem you're having.

vincent-thomas commented 11 months ago

I now did as suggested, and get another error:

⨯ Error: Unexpected end of CBOR data
    at checkedRead (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:225:25)       
    at Encoder.decodeMultiple (webpack-internal:///(rsc)/../../node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/decode.js:188:33)
    at Module.decodeFirst (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/helpers/iso/isoCBOR.js:30:29)
    at decodeCredentialPublicKey (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/helpers/decodeCredentialPublicKey.js:9:108)
    at verifySignature (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/helpers/verifySignature.js:24:113)
    at verifyAuthenticationResponse (webpack-internal:///(rsc)/../../node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse.js:166:101)
    at async POST (webpack-internal:///(rsc)/./src/app/api/webauthn/verify/route.ts:61:22)
    at async C:\Users\vincent\P\c\things\node_modules\.pnpm\next@14.0.3_@babel+core@7.23.3_react-dom@18.2.0_react@18.2.0\node_modules\next\dist\compiled\next-server\app-route.runtime.dev.js:6:62609 {
  incomplete: true,
  lastPosition: 1,
  values: [ -17 ]
}

Code edited:

  const url = new URL(APP_URL);

  const respons2 = await verifyAuthenticationResponse({
    response: body,
    expectedChallenge: challenge,
    expectedOrigin: url.origin,
    expectedRPID: url.hostname,
    authenticator: {
      credentialID: Uint8Array.from(
        account[0].device_id.split('').map((c) => c.charCodeAt(0)),
      ),
      credentialPublicKey: account[0].public_key,
      counter: -1,
    },
  });

  console.log(respons2);

account[0].public_key is of type Buffer

treeder commented 11 months ago

I'm getting the Unexpected end of CBOR data too. Tried to follow the docs exactly.

MasterKale commented 11 months ago

@vincent-thomas and/or @treeder Can you provide an example of your call to verifyAuthenticationResponse() with the actual values? For the bytes feel free to encode as hex or base64 or whatever, I can decode on my end as I try to understand what's going on. I just need something I can run locally.

treeder commented 11 months ago

@MasterKale here you go (hopefully safe to share this publicly ?):

{
  "id": "Z_lauVV2DC6KGN6J3MG1Fw",
  "rawId": "Z_lauVV2DC6KGN6J3MG1Fw",
  "response": {
    "authenticatorData": "xeiCdPuTO2iPRdPJ8QHq119e6SHi0yehQJjkxOXbNdkdAAAAAA",
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWVdKak1USXoiLCJvcmlnaW4iOiJodHRwczovL3JlZmFjdG9yZWQtcGFyYWtlZXQtd3J2cXY2cXdyZnhqdy0zMDAwLmFwcC5naXRodWIuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
    "signature": "MEQCIAfP3fFC8joFAWP0S7jxiz1fNTeCUeb4zaC6cqd7BGkBAiBOjYz8q-QnOJOTgW_UO2LjQKlkBSnQ7TqeL6o7RS4JXg",
    "userHandle": "gsKm2oCU8hgwexAu0pLir"
  },
  "type": "public-key",
  "clientExtensionResults": {},
  "authenticatorAttachment": "cross-platform"
}
MasterKale commented 11 months ago

(hopefully safe to share this publicly ?)

WebAuthn is very sensitive to user privacy, and the nature of public key cryptography means it's generally safe to share public keys, including in cases like this (and in fact I'll need you to share yours because the error being raised here is related to the value you're passing in for authenticator.credentialPublicKey.)

treeder commented 11 months ago

Ehh, sorry, that wasn't the full input to verifyAuthenticationResponse, here's the full input:

{
  "response": {
    "id": "Z_lauVV2DC6KGN6J3MG1Fw",
    "rawId": "Z_lauVV2DC6KGN6J3MG1Fw",
    "response": {
      "authenticatorData": "xeiCdPuTO2iPRdPJ8QHq119e6SHi0yehQJjkxOXbNdkdAAAAAA",
      "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWVdKak1USXoiLCJvcmlnaW4iOiJodHRwczovL3JlZmFjdG9yZWQtcGFyYWtlZXQtd3J2cXY2cXdyZnhqdy0zMDAwLmFwcC5naXRodWIuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
      "signature": "MEYCIQD69aeXacP_BW7ANxkR-3CfQiPCGBQBtqBebhWdUL0RlQIhAJbz-IfEXQ3MwJL_uzHfsAC1LWv77Maph_Wh3k-Mot-5",
      "userHandle": "gsKm2oCU8hgwexAu0pLir"
    },
    "type": "public-key",
    "clientExtensionResults": {},
    "authenticatorAttachment": "cross-platform"
  },
  "expectedChallenge": "YWJjMTIz",
  "expectedOrigin": "https://refactored-parakeet-wrvqv6qwrfxjw-3000.app.github.dev",
  "expectedRPID": "refactored-parakeet-wrvqv6qwrfxjw-3000.app.github.dev",
  "authenticator": {
    "credentialID": {
      "0": 103,
      "1": 249,
      "2": 90,
      "3": 185,
      "4": 85,
      "5": 118,
      "6": 12,
      "7": 46,
      "8": 138,
      "9": 24,
      "10": 222,
      "11": 137,
      "12": 220,
      "13": 193,
      "14": 181,
      "15": 23
    },
    "credentialPublicKey": {
      "0": 165,
      "1": 1,
      "2": 2,
      "3": 3,
      "4": 38,
      "5": 32,
      "6": 1,
      "7": 33,
      "8": 88,
      "9": 32,
      "10": 10,
      "11": 177,
      "12": 203,
      "13": 142,
      "14": 138,
      "15": 55,
      "16": 67,
      "17": 217,
      "18": 138,
      "19": 49,
      "20": 25,
      "21": 250,
      "22": 105,
      "23": 203,
      "24": 0,
      "25": 254,
      "26": 120,
      "27": 156,
      "28": 220,
      "29": 85,
      "30": 38,
      "31": 244,
      "32": 43,
      "33": 209,
      "34": 203,
      "35": 119,
      "36": 81,
      "37": 105,
      "38": 3,
      "39": 28,
      "40": 71,
      "41": 226,
      "42": 34,
      "43": 88,
      "44": 32,
      "45": 126,
      "46": 35,
      "47": 124,
      "48": 203,
      "49": 127,
      "50": 54,
      "51": 248,
      "52": 255,
      "53": 76,
      "54": 248,
      "55": 10,
      "56": 80,
      "57": 151,
      "58": 137,
      "59": 47,
      "60": 76,
      "61": 204,
      "62": 92,
      "63": 203,
      "64": 98,
      "65": 130,
      "66": 190,
      "67": 137,
      "68": 144,
      "69": 148,
      "70": 147,
      "71": 254,
      "72": 229,
      "73": 229,
      "74": 170,
      "75": 131,
      "76": 160
    },
    "counter": 0,
    "credentialDeviceType": "multiDevice",
    "credentialBackedUp": true
  }
}
treeder commented 11 months ago

Need anything else @MasterKale or is that what you were looking for?

MasterKale commented 11 months ago

Need anything else @MasterKale or is that what you were looking for?

@treeder That's what I was hoping for. I want to confirm something, though - are the values you're passing in for credentialID and credentialPublicKey of type object? They appear to be, based on the code, but I want to rule out you having passed those Uint8Array values through JSON.stringify() before pasting them here as an example.

treeder commented 11 months ago

I did pass the whole object through stringify yes.

On Mon, Dec 18, 2023 at 1:49 PM Matthew Miller @.***> wrote:

Need anything else @MasterKale https://github.com/MasterKale or is that what you were looking for?

@treeder https://github.com/treeder That's what I was hoping for. I want to confirm something, though - are the values you're passing in for credentialID and credentialPublicKey of type object? They appear to be, based on the code, but I want to rule out you having passed those Uint8Array values through JSON.stringify() before pasting them here as an example.

— Reply to this email directly, view it on GitHub https://github.com/MasterKale/SimpleWebAuthn/issues/490#issuecomment-1861323889, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAASQMR2CHM7NRZE6N4EZV3YKCF5DAVCNFSM6AAAAABAE2NQDWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNRRGMZDGOBYHE . You are receiving this because you were mentioned.Message ID: @.***>

MasterKale commented 11 months ago

Okay, so, if typeof authenticator.credentialID === 'object' and/or typeof authenticator. credentialPublicKey === 'object' are true, then that's your problem here.

Case in point, I ran the code in https://github.com/MasterKale/SimpleWebAuthn/issues/490#issuecomment-1859452074 and it errored out as expected:

console.log(
  (await verifyAuthenticationResponse({
    'response': {
      'id': 'Z_lauVV2DC6KGN6J3MG1Fw',
      'rawId': 'Z_lauVV2DC6KGN6J3MG1Fw',
      'response': {
        'authenticatorData': 'xeiCdPuTO2iPRdPJ8QHq119e6SHi0yehQJjkxOXbNdkdAAAAAA',
        'clientDataJSON':
          'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWVdKak1USXoiLCJvcmlnaW4iOiJodHRwczovL3JlZmFjdG9yZWQtcGFyYWtlZXQtd3J2cXY2cXdyZnhqdy0zMDAwLmFwcC5naXRodWIuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ',
        'signature':
          'MEYCIQD69aeXacP_BW7ANxkR-3CfQiPCGBQBtqBebhWdUL0RlQIhAJbz-IfEXQ3MwJL_uzHfsAC1LWv77Maph_Wh3k-Mot-5',
        'userHandle': 'gsKm2oCU8hgwexAu0pLir',
      },
      'type': 'public-key',
      'clientExtensionResults': {},
      'authenticatorAttachment': 'cross-platform',
    },
    'expectedChallenge': 'YWJjMTIz',
    'expectedOrigin': 'https://refactored-parakeet-wrvqv6qwrfxjw-3000.app.github.dev',
    'expectedRPID': 'refactored-parakeet-wrvqv6qwrfxjw-3000.app.github.dev',
    'authenticator': {
      'credentialID': { '0': 103, '1': 249, '2': 90, '3': 185, '4': 85, '5': 118, '6': 12, '7': 46, '8': 138, '9': 24, '10': 222, '11': 137, '12': 220, '13': 193, '14': 181, '15': 23 },
      'credentialPublicKey': { '0': 165, '1': 1, '2': 2, '3': 3, '4': 38, '5': 32, '6': 1, '7': 33, '8': 88, '9': 32, '10': 10, '11': 177, '12': 203, '13': 142, '14': 138, '15': 55, '16': 67, '17': 217, '18': 138, '19': 49, '20': 25, '21': 250, '22': 105, '23': 203, '24': 0, '25': 254, '26': 120, '27': 156, '28': 220, '29': 85, '30': 38, '31': 244, '32': 43, '33': 209, '34': 203, '35': 119, '36': 81, '37': 105, '38': 3, '39': 28, '40': 71, '41': 226, '42': 34, '43': 88, '44': 32, '45': 126, '46': 35, '47': 124, '48': 203, '49': 127, '50': 54, '51': 248, '52': 255, '53': 76, '54': 248, '55': 10, '56': 80, '57': 151, '58': 137, '59': 47, '60': 76, '61': 204, '62': 92, '63': 203, '64': 98, '65': 130, '66': 190, '67': 137, '68': 144, '69': 148, '70': 147, '71': 254, '72': 229, '73': 229, '74': 170, '75': 131, '76': 160 },
      'counter': 0,
    },
  })).verified,
);
// Uncaught (in promise) Error: Unexpected end of CBOR data

BUT, if I adhere to the API of verifyAuthenticationResponse() and make sure I'm passing in Uint8Array for credentialID and credentialPublicKey then the response verifies no problem:

console.log(
  (await verifyAuthenticationResponse({
    'response': {
      'id': 'Z_lauVV2DC6KGN6J3MG1Fw',
      'rawId': 'Z_lauVV2DC6KGN6J3MG1Fw',
      'response': {
        'authenticatorData': 'xeiCdPuTO2iPRdPJ8QHq119e6SHi0yehQJjkxOXbNdkdAAAAAA',
        'clientDataJSON':
          'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWVdKak1USXoiLCJvcmlnaW4iOiJodHRwczovL3JlZmFjdG9yZWQtcGFyYWtlZXQtd3J2cXY2cXdyZnhqdy0zMDAwLmFwcC5naXRodWIuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ',
        'signature':
          'MEYCIQD69aeXacP_BW7ANxkR-3CfQiPCGBQBtqBebhWdUL0RlQIhAJbz-IfEXQ3MwJL_uzHfsAC1LWv77Maph_Wh3k-Mot-5',
        'userHandle': 'gsKm2oCU8hgwexAu0pLir',
      },
      'type': 'public-key',
      'clientExtensionResults': {},
      'authenticatorAttachment': 'cross-platform',
    },
    'expectedChallenge': 'YWJjMTIz',
    'expectedOrigin': 'https://refactored-parakeet-wrvqv6qwrfxjw-3000.app.github.dev',
    'expectedRPID': 'refactored-parakeet-wrvqv6qwrfxjw-3000.app.github.dev',
    'authenticator': {
      // Coercing the `object` into a `Uint8Array`
      'credentialID': Uint8Array.from(Object.values({ '0': 103, '1': 249, '2': 90, '3': 185, '4': 85, '5': 118, '6': 12, '7': 46, '8': 138, '9': 24, '10': 222, '11': 137, '12': 220, '13': 193, '14': 181, '15': 23 })),
      // Coercing the `object` into a `Uint8Array`
      'credentialPublicKey': Uint8Array.from(Object.values({ '0': 165, '1': 1, '2': 2, '3': 3, '4': 38, '5': 32, '6': 1, '7': 33, '8': 88, '9': 32, '10': 10, '11': 177, '12': 203, '13': 142, '14': 138, '15': 55, '16': 67, '17': 217, '18': 138, '19': 49, '20': 25, '21': 250, '22': 105, '23': 203, '24': 0, '25': 254, '26': 120, '27': 156, '28': 220, '29': 85, '30': 38, '31': 244, '32': 43, '33': 209, '34': 203, '35': 119, '36': 81, '37': 105, '38': 3, '39': 28, '40': 71, '41': 226, '42': 34, '43': 88, '44': 32, '45': 126, '46': 35, '47': 124, '48': 203, '49': 127, '50': 54, '51': 248, '52': 255, '53': 76, '54': 248, '55': 10, '56': 80, '57': 151, '58': 137, '59': 47, '60': 76, '61': 204, '62': 92, '63': 203, '64': 98, '65': 130, '66': 190, '67': 137, '68': 144, '69': 148, '70': 147, '71': 254, '72': 229, '73': 229, '74': 170, '75': 131, '76': 160 })),
      'counter': 0,
    },
  })).verified,
  // true
);
treeder commented 11 months ago

Ahh, that's helpful. I think I see what I was doing wrong now, I was saving the authenticator in previous steps as a JSON object (stringified) not keeping them as bytes.

You're solution here fixed it! I added a couple lines like this and it works now:

authenticator.credentialID = Uint8Array.from(Object.values(authenticator.credentialID))
    authenticator.credentialPublicKey = Uint8Array.from(Object.values(authenticator.credentialPublicKey))
    let vdata = {
        response: body.credential,
        expectedChallenge: challenge,
        expectedOrigin: "https://" + globals.rpId,
        expectedRPID: globals.rpId,
        authenticator: authenticator,
    }
    let verification = null
    try {
        verification = await verifyAuthenticationResponse(vdata);
    } catch (error) {
        console.error(error);
        return c.json({ error: { message: error.message } }, 401)
    }
MasterKale commented 11 months ago

@vincent-thomas I don't know if @treeder had the same issue as you. His problem's solved, if yours is still valid then let me know otherwise I'll close this out.