-
Notifications
You must be signed in to change notification settings - Fork 409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add WebAuthn authentication flow #6792
Conversation
dd9fed5
to
f6dd579
Compare
8c68aa2
to
d404dab
Compare
3fa39a0
to
095b654
Compare
095b654
to
06109e0
Compare
) | ||
result_json = json.loads(result.decode()) | ||
user_handles: set[str] = {x["user_handle"] for x in result_json} | ||
assert len(user_handles) == 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we assert that there's only a single user_handle associated with a given email? I thought we wanted to allow you to use the same email with multiple authenticators: #6791 (comment)
Unless they should all have the same user_handle (which I guess was the reason for dropping the exclusive constraint on (.email, .user_handle)
?), in which case in create_registration_options_for_email
we should check if there's already a WebAuthnFactor
for that email and re-use the user_handle
. I don't know if there's some security issue with being able to retrieve an email's user_handle like that though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we're supposed to use the same user_handle if the user is "the same user" to allow multiple WebAuthn factors for a single user. You're right that I'm missing that check from the create_registration_options_for_email
but I need to think about the UX here a bit more.
The ostensible reason for allowing multiple factors is to allow what are basically back-up factors (or multi-factors) so you can seamlessly move between passkeys, hardware authenticators, etc. but in our "registration" flow it's not super obvious that you might have multiple identities with the same email which point to a single User
in the downstream application because we kind of leave that up to the application developer to do.
But, yeah I think at the very least we should reuse the same user handle if you provide an existing email, will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From testing it seems there's another issue with this assert. If the email isn't registered, then there's no user-handles,the assert fails and we return an ise. I'm not sure what the correct error should be here? I know we're trying not to leak the existence of emails, but I don't think we can get around that as we can only return a challenge if we have the stored credentials for an email.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good catch: assert is the wrong tool here, we need to raise a specific exception for proper error handling.
As far as data leakage is concerned, I think we talked about register
being fine to leak whether an email is signed up or not, but authenticate
would always pretend it succeeds. From that perspective, I think we can return a sensible error here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jaclarke ⚡ https://github.com/edgedb/edgedb/pull/6792/files/40d636291fca7bb5db36ebd142e7e58fa2214bdd..1431063a019694d9028966b1ae1cbce2d8c6e93b
Turns out I needed a bit more work to ensure the invariant that multiple factors with the same email
have the same user_handle
. Added a test for the new trigger.
}); | ||
|
||
if (!authenticateResponse.ok) { | ||
console.error( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this handles the case where email verification is required, so we'll end up returning a misleading error that webauthn auth failed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Now that we have stable type names in our error json, I can detect this case and at least throw a more helpful error. We still navigate you to the "error" state of the sign in page in the email+password case, so I think that throwing the correct error should do the right thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
00cd8cd
to
04a063e
Compare
b3608e1
to
91c23fc
Compare
91c23fc
to
40d6362
Compare
1f14960
to
60a5c24
Compare
60a5c24
to
1431063
Compare
edb/lib/ext/auth.edgeql
Outdated
select detached ext::auth::WebAuthnFactor | ||
filter .email = __new__.email | ||
and not .id = __new__.id | ||
).user_handle) ?? true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor thing, but I think ?? true
probably isn't needed. If the result of the select is an empty set, the assertion just won't run anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL about the empty set behavior of assert
. I'll add an example to the documentation for assert
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it just follows the normal set behaviour, where if the function arg isn't set of
(like in count
, for example) then it's expanded out. So assert({true, false})
is executed as {assert(true), assert(false)}
, and similarly assert({})
becomes {}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. The documentation hints at this to since it says:
If the input bool is false, assert raises a QueryAssertionError. Otherwise, this function returns true. (emphasis mine)
I think it'd just be helpful to show some examples of calling this function with a sets to show that. I'll follow up.
assert len(user_handles) == 1 | ||
if len(user_handles) > 1: | ||
raise errors.WebAuthnAuthenticationFailed( | ||
"Multiple user handles found for the email." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could still be an assert. It's really more of an ISE if there's multiple user handles, since the db should enforce that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, that's a good point. I'd want to still log a useful error message in the ISE, so I'll throw something together.
Empty set isn't evaluated by `assert`, so this passes when the select returns an empty set. And since `=` returns the empty set for comparison, the logic is the same.
Builds on #6791
Authentication is almost the same as registration:
challenge
andallowCredentials
that identifies the user based on the providedemail
+user_handle
assertion
which includes the signed challenge, and other information about the authenticator.Like the registration flow, it uses PKCE to do the final exchange of the actual token.