WebAuthn cryptically not working in production but works perfectly in development

My debugger could parse a clientDataJSON that starts with an "e" just fine so I think it’s a coincidence:

Screen Shot 2022-10-25 at 1.03.39 PM

Yeah, I would’ve thought it happened automatically - and it is definitely storing data somewhere, and I just double checked that it’s using the right connection string, so I have no clue what’s happening

Have you ever used the patch-package package? It’ll let you patch some functionality into a lib you’re using and then it’ll apply it whenever you yarn install, even in production. You could try adding some debug/console statements around the code that’s erroring and try to figure out what some of the variables contain before it blows up. Any console.log statements should hopefully show up in Vercel’s log output.

You’ll have to poke through the dist version of the code and see if you can figure out where to put them so that it matches up with what’s in src, but right before that line that @IAmKale pointed out (line 884) seems like a great place to start.

Sorry this is so much work…we haven’t got any other reports like this, but it’s possible no one else has tried deploying WebAuthn to production yet!

1 Like

Wait sorry I don’t know why I can’t see any of this on Railway, but using TablePlus I was able to connect to the correct database - I can confirm that nothing is being created in the UserCredential table

Yeah, I think that’s probably my next step. And no worries! Thank you for all the support, and thank you for building Redwood!

I’m working on some other things right now, but then my next steps are:

  1. See if I can repro this in dev by doing what you mentioned earlier in terms of adding a host entry
  2. If I can’t, patch the package to increase logging
1 Like

@rob, is there a recommended way to set up Redwood to use HTTPS locally? just setting up the localhost name doesn’t work, it skips webauthn because it’s not a secure site

Hi @arimendelow, you might want to check out ngrok.io. Decent tool for setting up temporary, publicly accessible HTTPS tunnels to your local dev system

3 Likes

Second that, ngrok is perfect for this!

1 Like

oh that’s a great call!!! i totally forgot about ngrok - will give that a go, thanks!

1 Like

We’ve got someone on Discord saying they’ve used WebAuthn successfully through ngrok:

  1. I did see that Discourse thread and am interested in that topic myself. I don’t think we’ll be able to deploy this anytime real soon, but I’ll keep following to see if I can contribute anything useful. FWIW, it seems to be working ok for me locally via ngrok.

As far as the browser is concerned there shouldn’t be any difference between browsing to the site via ngrok and browsing to a real, deployed app on the internet.

Could this somehow be a CORS thing? Are your web and api sides being served from different domains?

Still working on testing with ngrok - the free version only lets you serve one side at a time, and for whatever reason I can’t get CORS to be happy when I serve only the API side with it. Tried following this: Cross-Origin Resource Sharing | RedwoodJS Docs

But no go. Will probably take a break from trying that soon and try out the increased logging on the deployed API side.

It’s definitely not a CORS thing in my deployed situation, the browser is usually very vocal about that and either way my sides are being served from the same domain (just doing the OOB Vercel deployment).

The hunt continues!!

1 Like

Another trick you can do is set up a DNS entry to point to an intranet IP address. I do this when testing out my WebAuthn library on mobile:

  1. A “dev.example.com” A record in your domain’s DNS points to 192.168.1.100 (or whatever your machine’s local IP is)
  2. Generate certificates for this domain using certbot and the following command:
    • sudo certbot -d dev.example.com --manual certonly --preferred-challenges dns
  3. Follow the instructions, then take note of the two file paths to the privkey.pem and fullchain.pem paths.
  4. Pass those file paths to Node’s https.createServer() when starting the server. You can see an example of how to do this here.
  5. Your server becomes available for all devices on your home network at https://dev.example.com

This is a little bit more involved that some other solutions, but it’s worked reliably for me for the last two years I’ve worked on SimpleWebAuthn. I’ll bet you can do something like this (barring any RedwoodJS-specific ways of using your own SSL certs and custom domain) to test your code locally on a non-localhost, HTTPS-served URL that won’t expose the instance to the internet.

1 Like

Okay, made a patch to add a bunch of console logs on: /spoonjoy/node_modules/@redwoodjs/api/dist/functions/dbAuth/DbAuthHandler.js

here’s the output locally:

in webAuthnRegister, user: {
  id: 'cl9rg3xrx0000qvhgmvz2nl49',
  username: 'ari',
  webAuthnChallenge: 'CSYoMESnNTD4Ar2nrFaZWmRT7nkAs3gXbtuesCzbSIw'
}
in webAuthnRegister, this.event: {
  httpMethod: 'POST',
  headers: {
    cookie: 'session=U2FsdGVkX1/RiaQoArIxvXxyMh0jEO5aaw+xmMgWmAeaTChU6O8PoZQztW2AjOUSxfedkcEsBqvSuQFnZd9Q4CnKFi6CM490jAY5Tq9OA0F6kiJx7dWgG5csQn00lMhB',
    'accept-language': 'en-US,en;q=0.9,he;q=0.8',
    'accept-encoding': 'gzip, deflate, br',
    referer: 'http://localhost:8910/login',
    'sec-fetch-dest': 'empty',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'same-origin',
    origin: 'http://localhost:8910',
    accept: '*/*',
    'content-type': 'application/json',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
    'sec-ch-ua-mobile': '?0',
    dnt: '1',
    'sec-ch-ua-platform': '"macOS"',
    'sec-ch-ua': '"Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"',
    'content-length': '743',
    connection: 'keep-alive',
    host: 'localhost:8910'
  },
  path: '/auth',
  queryStringParameters: {},
  requestContext: { requestId: 'req-a', identity: { sourceIp: '::1' } },
  body: '{"method":"webAuthnRegister","id":"YRNF0dzjgImf_Z4m0bWOo6qNVNn_9bxhVKB64ggYGFI","rawId":"YRNF0dzjgImf_Z4m0bWOo6qNVNn_9bxhVKB64ggYGFI","response":{"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIGETRdHc44CJn_2eJtG1jqOqjVTZ__W8YVSgeuIIGBhSpQECAyYgASFYIJ5XvsIcksNGINlKs9ZSLMbeqqUlVxBAvo7E48BbmDcEIlgg1qQHuiTM2xEUvxfWt26GNrE9IgTRF4tavGPPCh0XmKg","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ1NZb01FU25OVEQ0QXIybnJGYVpXbVJUN25rQXMzZ1hidHVlc0N6YlNJdyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODkxMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"platform","transports":["internal"]}',
  isBase64Encoded: false
}
in webAuthnRegister, jsonBody: {
  method: 'webAuthnRegister',
  id: 'YRNF0dzjgImf_Z4m0bWOo6qNVNn_9bxhVKB64ggYGFI',
  rawId: 'YRNF0dzjgImf_Z4m0bWOo6qNVNn_9bxhVKB64ggYGFI',
  response: {
    attestationObject: 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIGETRdHc44CJn_2eJtG1jqOqjVTZ__W8YVSgeuIIGBhSpQECAyYgASFYIJ5XvsIcksNGINlKs9ZSLMbeqqUlVxBAvo7E48BbmDcEIlgg1qQHuiTM2xEUvxfWt26GNrE9IgTRF4tavGPPCh0XmKg',
    clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ1NZb01FU25OVEQ0QXIybnJGYVpXbVJUN25rQXMzZ1hidHVlc0N6YlNJdyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODkxMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0'
  },
  type: 'public-key',
  clientExtensionResults: {},
  authenticatorAttachment: 'platform',
  transports: [ 'internal' ]
}
in webAuthnRegister, options: {
  credential: {
    method: 'webAuthnRegister',
    id: 'YRNF0dzjgImf_Z4m0bWOo6qNVNn_9bxhVKB64ggYGFI',
    rawId: 'YRNF0dzjgImf_Z4m0bWOo6qNVNn_9bxhVKB64ggYGFI',
    response: {
      attestationObject: 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIGETRdHc44CJn_2eJtG1jqOqjVTZ__W8YVSgeuIIGBhSpQECAyYgASFYIJ5XvsIcksNGINlKs9ZSLMbeqqUlVxBAvo7E48BbmDcEIlgg1qQHuiTM2xEUvxfWt26GNrE9IgTRF4tavGPPCh0XmKg',
      clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ1NZb01FU25OVEQ0QXIybnJGYVpXbVJUN25rQXMzZ1hidHVlc0N6YlNJdyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODkxMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0'
    },
    type: 'public-key',
    clientExtensionResults: {},
    authenticatorAttachment: 'platform',
    transports: [ 'internal' ]
  },
  expectedChallenge: 'CSYoMESnNTD4Ar2nrFaZWmRT7nkAs3gXbtuesCzbSIw',
  expectedOrigin: 'http://localhost:8910',
  expectedRPID: 'localhost',
  requireUserVerification: true
}
in webAuthnRegister, verification: {
  verified: true,
  registrationInfo: {
    fmt: 'none',
    counter: 0,
    aaguid: 'adce0002-35bc-c60a-648b-0b25f1f05503',
    credentialID: <Buffer 61 13 45 d1 dc e3 80 89 9f fd 9e 26 d1 b5 8e a3 aa 8d 54 d9 ff f5 bc 61 54 a0 7a e2 08 18 18 52>,
    credentialPublicKey: <Buffer a5 01 02 03 26 20 01 21 58 20 9e 57 be c2 1c 92 c3 46 20 d9 4a b3 d6 52 2c c6 de aa a5 25 57 10 40 be 8e c4 e3 c0 5b 98 37 04 22 58 20 d6 a4 07 ba 24 ... 27 more bytes>,
    credentialType: 'public-key',
    attestationObject: <Buffer a3 63 66 6d 74 64 6e 6f 6e 65 67 61 74 74 53 74 6d 74 a0 68 61 75 74 68 44 61 74 61 58 a4 49 96 0d e5 88 0e 8c 68 74 34 17 0f 64 76 60 5b 8f e4 ae b9 ... 144 more bytes>,
    userVerified: true,
    credentialDeviceType: 'singleDevice',
    credentialBackedUp: false,
    authenticatorExtensionResults: undefined
  }
}

in prod:

in webAuthnRegister, user: {
  id: 'cl9rixd9q000708lcf79jb5vp',
  username: 'ari',
  webAuthnChallenge: '3OhUAoJXrhrop3HLw50YSpXj-W7me3_k_Z95D2mq59A'
}
in webAuthnRegister, this.event: {
  resource: '/{proxy+}',
  path: '/api/auth',
  httpMethod: 'POST',
  body: 'eyJtZXRob2QiOiJ3ZWJBdXRoblJlZ2lzdGVyIiwiaWQiOiJKMHV1djBXUEpEQmFoSTN6OVo3LVkzNy1sQzAxaGpmOWdyWjBHRzVhZnVBIiwicmF3SWQiOiJKMHV1djBXUEpEQmFoSTN6OVo3LVkzNy1sQzAxaGpmOWdyWjBHRzVhZnVBIiwicmVzcG9uc2UiOnsiYXR0ZXN0YXRpb25PYmplY3QiOiJvMk5tYlhSa2JtOXVaV2RoZEhSVGRHMTBvR2hoZFhSb1JHRjBZVmlrNTZONUx5c2hLUkp0eXR1R0lTSE9TXzg5SEM3VWRSRnNHelJYVTFnNDJEUkZBQUFBQUszT0FBSTF2TVlLWklzTEpmSHdWUU1BSUNkTHJyOUZqeVF3V29TTjhfV2VfbU4tX3BRdE5ZWTNfWUsyZEJodVduN2dwUUVDQXlZZ0FTRllJTUkwMkFQS3J2c0R4M3JmXzlLNnNBYURBYjJiOHBrN1BZcmdhSjNFQUhCQUlsZ2dZR1VOYlNrdmx6UUpuaG9ZLWlNMWlNNFk4a2RycWUxdkNNTjhKQ08wQXlRIiwiY2xpZW50RGF0YUpTT04iOiJleUowZVhCbElqb2lkMlZpWVhWMGFHNHVZM0psWVhSbElpd2lZMmhoYkd4bGJtZGxJam9pTTA5b1ZVRnZTbGh5YUhKdmNETklUSGMxTUZsVGNGaHFMVmMzYldVelgydGZXamsxUkRKdGNUVTVRU0lzSW05eWFXZHBiaUk2SW1oMGRIQnpPaTh2YzNCdmIyNXFiM2t1WVhCd0lpd2lZM0p2YzNOUGNtbG5hVzRpT21aaGJITmxmUSJ9LCJ0eXBlIjoicHVibGljLWtleSIsImNsaWVudEV4dGVuc2lvblJlc3VsdHMiOnt9LCJhdXRoZW50aWNhdG9yQXR0YWNobWVudCI6InBsYXRmb3JtIiwidHJhbnNwb3J0cyI6WyJpbnRlcm5hbCJdfQ==',
  isBase64Encoded: true,
  queryStringParameters: {},
  multiValueQueryStringParameters: [Object: null prototype] {},
  headers: {
    host: 'spoonjoy.app',
    'x-real-ip': '97.126.31.97',
    'sec-ch-ua-mobile': '?0',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-dest': 'empty',
    referer: 'https://spoonjoy.app/login',
    'x-vercel-forwarded-for': '97.126.31.97',
    'sec-ch-ua': '"Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"',
    origin: 'https://spoonjoy.app',
    'sec-ch-ua-platform': '"macOS"',
    'x-forwarded-host': 'spoonjoy.app',
    accept: '*/*',
    'x-vercel-ip-timezone': 'America/Los_Angeles',
    'x-forwarded-for': '97.126.31.97',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
    'content-type': 'application/json',
    'x-vercel-proxied-for': '97.126.31.97',
    cookie: 'session=U2FsdGVkX19a6cnBGsK9Bq2LsWyXX7H6N7BphHb0q69IWcFM9nRndEH8mCgB0R+kM7afTWUfuQzWK0aqObHZDgp5p+aDsNhN5xPc2wIzjqwfhZuDNac8PWQywwU7YiD9',
    'x-vercel-ip-latitude': '47.5741',
    forwarded: 'for=97.126.31.97;host=spoonjoy.app;proto=https;sig=0QmVhcmVyIDI2MGYwODRjNTMxOTYwNjE2ZmIxOWViNjU2Yjc0YzY3MWEwMmJkOTc4NjRjMjZkZTJkYjQxZmU4ZjA5MmY0N2Y=;exp=1666903183',
    'x-vercel-id': 'pdx1::tn2w6-1666902883260-61ac8fc7bdcf',
    'x-vercel-ip-longitude': '-122.3975',
    'x-vercel-deployment-url': 'spoonjoy-jzen7pgsx-ari-spoonjoyapp.vercel.app',
    'x-vercel-ip-country-region': 'WA',
    'sec-fetch-mode': 'cors',
    'x-vercel-ip-country': 'US',
    'x-forwarded-proto': 'https',
    'content-length': '742',
    'accept-encoding': 'gzip, deflate, br',
    'x-vercel-proxy-signature': 'Bearer 260f084c531960616fb19eb656b74c671a02bd97864c26de2db41fe8f092f47f',
    'accept-language': 'en-US,en;q=0.9,he;q=0.8',
    'x-vercel-ip-city': 'Seattle',
    dnt: '1',
    'x-vercel-proxy-signature-ts': '1666903183'
  }
}

so it’s not a coincidence that the token starts with an e! in prod something is setting this.event.body to the token rather than the proper payload

oh right it’s supposed to be, because it’s base64 encoded in prod. the question is why it’s not being decoded, looking…

HMMMMM That body is way longer than the clientDataJSON key in the dev response. Where are you deployed? Something in the back of my memory tells me that certain deploy targets actually base64 the body before giving it to a Lambda function. That body is so long it’s making me wonder if that’s actually ALL the body, just base64 encoded.

While you’re patch-packaging, want to try base64 decoding this.event.body right before JSON.parse is called?

1 Like

There may even be a helper somewhere, isBase64Encoded() that’ll let you conditionally do it only in production…

Found this in our docs: Webhooks | RedwoodJS Docs dbAuth isn’t a “webhook” persay, but it is a serverless function, which is what webhooks are as well. Maybe dbAuth itself needs a built-in decoder for the event body that checks for base64-ness and decodes if necessary!

Deployed on Vercel! Right, it’s weird that locally body is so long. And you’re right, it’s not clientDataJSON, but it does contain it - I decoded the prod body, and this is what came out:

{"method":"webAuthnRegister","id":"J0uuv0WPJDBahI3z9Z7-Y37-lC01hjf9grZ0GG5afuA","rawId":"J0uuv0WPJDBahI3z9Z7-Y37-lC01hjf9grZ0GG5afuA","response":{"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVik56N5LyshKRJtytuGISHOS_89HC7UdRFsGzRXU1g42DRFAAAAAK3OAAI1vMYKZIsLJfHwVQMAICdLrr9FjyQwWoSN8_We_mN-_pQtNYY3_YK2dBhuWn7gpQECAyYgASFYIMI02APKrvsDx3rf_9K6sAaDAb2b8pk7PYrgaJ3EAHBAIlggYGUNbSkvlzQJnhoY-iM1iM4Y8kdrqe1vCMN8JCO0AyQ","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiM09oVUFvSlhyaHJvcDNITHc1MFlTcFhqLVc3bWUzX2tfWjk1RDJtcTU5QSIsIm9yaWdpbiI6Imh0dHBzOi8vc3Bvb25qb3kuYXBwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"},"type":"public-key","clientExtensionResults":{},"authenticatorAttachment":"platform","transports":["internal"]}

So yup exactly - we need to be doing something like _parseBody()

It has a decoder that does something like that! Check out _parseBody()