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

Getting "CredentialContainer request is not allowed" when trying to create a second passkey #606

Closed SnickerChar closed 2 months ago

SnickerChar commented 2 months ago

Describe the issue

First, thanks for this library. I did it by hand for a demonstration and this just makes webauthn so much easier to use. On our system, we have 3 workflows with passkeys- register a passkey as a new user, login with an existing passkey, and register a new passkey as an authenticated user. I have the first two flows working great, but I'm having trouble on the third flow. In this case, we've issued the user a JWT authentication token, and they make an API call to the server and ends up making a call to generateRegistrationOptions(). We generate a challenge and store it in Redis connected to their internal user ID, and lookup all the existing passkeys they have registered with us from the DB; currently, only 1. The credentialID of those passkeys is added to excludeCredentials along with the transports, as per your examples.

This is returned to the client, but the startRegistration() call is failing with a NotAllowedError: CredentialContainer request is not allowed.

Removing the excluded credentials on the server side doesn't change anything. Removing the passkey from the OS allows me to re-register, although some refreshes are usually needed.

Reproduction Steps

  1. Create a passkey on firefox with the default settings
  2. Try to create a second passkey using the same settings and a different name
  3. startRegistration fails with NotAllowedError

Expected behavior

startRegistration() should offer to create a new passkey

Code Samples + WebAuthn Options and Responses

Here's a sample registration JSON from generateRegistrationOptions(); excludeCredentials is populated on the logged-in version but it doesn't make a difference, only the presense of a passkey in the OS.

{"challenge":"DL46LBPuhMmtVAd-LeYW-BLhSNFagZLQo97Ph8q6Nrk","rp":{"name":"Snickerdoodle Labs","id":"localhost"},"user":{"id":"HWynNy3Cc45A6tbXr9Evar8Y7tVtrOvH1XaUeF7Uk1w","name":"TEST","displayName":""},"pubKeyCredParams":[{"alg":-8,"type":"public-key"},{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"}],"timeout":60000,"attestation":"none","excludeCredentials":[],"authenticatorSelection":{"residentKey":"preferred","userVerification":"preferred","authenticatorAttachment":"platform","requireResidentKey":false},"extensions":{"credProps":true}}

Dependencies

SimpleWebAuthn Libraries

@simplewebauthn/browser@10.0.0 @simplewebauthn/server@10.0.1

Additional context

One thing I can't figure out is that when registering a passkey, I get no options. It only asks me to put in my PIN, and never asks where I'd like to save the passkey. I'm just using the default AuthenticatorSelection options from the example, so:

residentKey: "preferred", userVerification: "preferred", authenticatorAttachment: "platform",

But I'd like to store the passkey on my Yubikey and am never given an option. I think this may be why it's not allowing the creation of a second passkey, even with a different username. Obviously, there's something about the authenticator selection options I'm not grasping.

SnickerChar commented 2 months ago

Oh, and BTW, in your docs, there's another bit that goes completely over my head. In the Authentication Flow you describe, you already know who the user is, which seems to me like it requires the user to be already authenticated. That seems circular. I know there's a case, and we'll be using it, for doing the authentication flow with an already authenticated user. Particularly with a custom challenge, for doing some interesting crypto signature verification stuff. But even tearing apart the sample repo has me still confused on this one. In my implementation, I leave allowedCredentials as [], and just lookup the passkey in my DB based on the credentialId, so you should be able to just walk up and login with any known passkey, assuming I could register multiples...

SnickerChar commented 2 months ago

Here's the JSON from generateRegistrationOptions() for the second passkey:

{"challenge":"WKm30aJOa_ZC2rsDhOaXAm2lV12k42Oxt82V8F-SW7E","rp":{"name":"Snickerdoodle Labs","id":"localhost"},"user":{"id":"LstQsgCZZByMR12d8jwW4NBlbk1YlwfqAsDfFljg62U","name":"/Foo","displayName":""},"pubKeyCredParams":[{"alg":-8,"type":"public-key"},{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"}],"timeout":60000,"attestation":"none","excludeCredentials":[{"id":"6PVU1plCxVrtGHu0_92N3hyz3Nuud3pfcoTHx2pugKc","transports":[""],"type":"public-key"}],"authenticatorSelection":{"residentKey":"preferred","userVerification":"preferred","authenticatorAttachment":"platform","requireResidentKey":false},"extensions":{"credProps":true}}

SnickerChar commented 2 months ago

Removing the "authenticatorAttachment" param fixed the key storage location stuff, I can now store it both locally or on my phone/yubikey. Got that figured out.

SnickerChar commented 2 months ago

OK, I got it figured out. It failed on Firefox because with the authenticatorAttachment set to anything, it can only create 1 passkey per authenticator. This is the same on Chrome, but in FF, it fails before it shows you the dialog. On Chrome, this would go through to the authenticator and fail there. FF is just a tad smarter it seems. Once I cleared the authenticatorAttachment, I was able to make a passkey on my local machine and another on my phone, and everything works just dandy.