Skip to content
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

Can RPs assume that InvalidStateError for create() means an excludeCredentials match? #1566

Closed
lgarron opened this issue Feb 9, 2021 · 11 comments
Assignees
Labels
stat:Discuss subtype:Inquiry Inquiry wrt API and/or protocol capabilities/features, or WG / pub process, or spec editorial stuff

Comments

@lgarron
Copy link
Contributor

lgarron commented Feb 9, 2021

Motivation

Suppose that a user has a platform authenticator registered in one browser, and would like to use it in another browser that shares the authenticator (e.g. using Windows Hello in Edge and Chrome). To an RP with a fresh browser session, the "same platform authenticator in a different browser" case is indistinguishable from the "entirely new device" case. But since some platform authenticators make it impossible to have multiple credentials for the same RP, it's important that the RP can re-associate a credential across browsers instead of overwriting an existing one.

The most useful thing for the RP would be a way to query "does a registration already exist?", which I presume is out of the question. (Is it?)

The second-most useful thing would be a way for the RP to "create or get" a credential. However, this was not seen as an area of improvement for the spec when I brought it up. (But I'd be very glad to see it revisited. I don't see a significant privacy issue, and it would avoid the need for the kinds of workarounds that led to me filing this issue.)

As it stands, if a RP does not want to place 100% of the responsibility of keeping track of registrations to their users, they have to ask the user to create a new credential and offer a fallback if the user may be trying to register an already-registered credential.

It would be nice to minimize the number of choices we offer to the user (i.e. optimistically show only the create option), and utilize the create error to provide the most useful feedback and followup actions for the user.


InvalidStateError

Based on talking to some browser authors, it seems that the spec is currently designed so that an InvalidStateError response for navigator.credentials.get() means the registration failed because all authenticators that match the selection criteria (e.g. user-verifying platform authenticator) already have a registration for the RP ID.

However, Safari currently has a bug for this situation that makes the "registration already exists" result the same as if the user had canceled the operation: https://bugs.webkit.org/show_bug.cgi?id=219813
In addition, the spec seems to make no guarantees that InvalidStateError will only cover this particular meaning in the future. The general meaning of InvalidStateError is certainly vague enough to apply in other situations.

Would it be possible to clarify whether RPs can rely on this?

We'd like to know whether Apple's implementation should be considered a bug, or if we need to weaken clarity of our platform authenticator (re-)registration flow to accommodate for the possibility that InvalidStateError does not indicate an existing registration.

@akshayku
Copy link
Contributor

akshayku commented Feb 9, 2021

I think Apple's behaviour is a bug. InvalidStateError response from create() means that there exists a credential on the authenticator from the excludelist.

cc @alanwaketan

@nicksteele nicksteele self-assigned this Feb 10, 2021
@equalsJeffH
Copy link
Contributor

on 2021-02-10 call:
@nicksteele signed up to submit a webkit bug on this.

@lgarron
Copy link
Contributor Author

lgarron commented Feb 10, 2021

on 2021-02-10 call:
@nicksteele signed up to submit a webkit bug on this.

Does https://bugs.webkit.org/show_bug.cgi?id=219813 cover it?

@nicksteele
Copy link
Contributor

Hey Lucas,
I would say that while you can use the InvalidStateError as inference that the user is using the same platform authenticator, I am a bit confused about the use-case: it seems like you're trying to use this error to determine if the user is already registered. Why not just check with the user by supplying a get request? Also, In the ideal case, a user registers with an RP and closes the browser client, then opens another client and navigates to the previously registered RP in the new client. Upon the user hitting login, the authenticator should be able to use the previously created credential for that RP in the new client browser. Is this not the case? What am I missing? The pandemic has turned my brain into pudding.

Your second point also seems to be laid out in #1568 and thank you for submitting this issue to WebKit 🙇

@equalsJeffH equalsJeffH added stat:Discuss subtype:Inquiry Inquiry wrt API and/or protocol capabilities/features, or WG / pub process, or spec editorial stuff labels Feb 17, 2021
@lgarron
Copy link
Contributor Author

lgarron commented Feb 19, 2021

I have to say I'm a bit bewildered by the closing of this issue.

Do I understand from the closing of this issue that RPs cannot rely on InvalidStateError to indicate that registration failed due to an excluded credential, and that there is no interest from the spec authors to clarify this in either direction?


I would say that while you can use the InvalidStateError as inference that the user is using the same platform authenticator, I am a bit confused about the use-case: it seems like you're trying to use this error to determine if the user is already registered. Why not just check with the user by supplying a get request?

Because the time when we need to do this is indistinguishable from a situation where a user is on a new device. It sounds like you're suggesting the following registration for for a new trusted device:

  • Prompt the user to authenticate with their device.
  • Proceed to registration only if that prompt fails.

This is undesirable because:

  • The client would see an error every time they try to register a new device. The RP has no control over the UI for this, so we can't do much to reduce the potential for confusion.
  • We would like to avoid showing too many prompts to the user, if possible.

We're currently trying to use heuristics to default to the create flow when possible. But if our heuristics are wrong, we need a way to catch it, and it would be very useful if we could tell if this is due to an existing registration (and which one).

Also, In the ideal case, a user registers with an RP and closes the browser client, then opens another client and navigates to the previously registered RP in the new client. Upon the user hitting login, the authenticator should be able to use the previously created credential for that RP in the new client browser. Is this not the case? What am I missing? The pandemic has turned my brain into pudding.

Indeed, the authenticator can use the previously created credential if we know to prompt for it. This requires either:

  • Knowing which user is authenticating, and having authenticated enough for it to be safe to send down allowCredentials.
    • For us, "having authenticated enough" means they've entered their password at least once.
  • The user has client-side credentials and has a for the site to use them without disrupting the normal (password-based) authentication flow. It sounds like Add support for non-modal UI #1545 is the most likely path for that.

In general, neither makes it possible possible to prompt for the credential in a fresh browser profile until a user already has full authenticated.

@lgarron
Copy link
Contributor Author

lgarron commented Feb 19, 2021

I just realized that there may be an assumption that may lead to miscommunication: whether 2FA is required.

Currently, a GitHub user with "normal 2FA enabled" must use the following to sign in:

  • A password AND a 2FA challenge (SMS, OTP, or security key).

A user with "normal 2FA disabled":

  • A password.

We intend to start by using trusted devices as described here, to make re-authentication on a device easy. If a user registers or uses a trusted device in a given browser profile, then we we will remember their device in a cookie even after logout, and show them a trusted device login prompt (instead of a password login) when they try to log in next time1.

➡️ But we want to make this feature available to all users, **even those who do not have "normal 2FA enabled"2. ⬅️

This means a GitHub user with "normal 2FA enabled" must use the following to sign in:

  • Either of:
    • A user-verifying authenticator.
    • A password AND a 2FA challenge (SMS, OTP, security key*).

A user with "normal 2FA disabled":

  • Either of:
    • A user-verifying authenticator.
    • A password.

It's important that the registration of a trusted device does not turn a "normal 2FA disabled" user into a 2FA user, because we want this feature to be low-friction and only "value added". We don't want to force them to sign up for for "normal 2FA" in order to register a trusted device, and we don't want to require 2FA for all logins as soon as they have a trusted device somewhere. (If that device is a platform authenticator3, they'd be locked out of signing into any other device!).

This means that they will only need to provide their password in a fresh browser profile in order to be completely logged in. And since we only prompt them for their password, this means we don't prompt them for their trusted device, and we can't tell if this new browser profile has a trusted device registered, i.e. if they could log in using a trusted device the next time. We could:

  • Make a wild guess about whether they have a trusted device registered (e.g. based on their current UA and isUserVerifyingPlatformAuthenticatorAvailable() compared to past registrations and their UAs).
  • Prompt them to register a device.

We're currently trying to figure out how to deal with the latter. As described above, we need to handle both the case where there is already a trusted device registered for this browser profile (in which a get prompt is appropriate) or not (in which a create prompt is appropriate). This issue is about the situation where the user goes through a create prompt first, when get was the appropriate choice. We'd like to follow up by showing the get prompt we "should" have shown in the first place, but we'd like to be confident that we're showing the get fallback because of an excluded registration — not because of an arbitrary registration error.

Does that help clarify our concern?

P.S. Our password field is directly below the username field as part of the same prompt. We do not intend to change this.

Footnotes

  1. As recommended by HowToFIDO and WebKit's blog post.

  2. A trusted device can be used here as well, but that's not relevant to the discussion about users with "normal 2FA disabled".

  3. We plan to require this, as incentivized by the peerless existence of isUserVerifyingPlatformAuthenticatorAvailable().

@arshadnoor
Copy link

It's important that the registration of a trusted device does not turn a user without "normal 2FA" into a 2FA user, because we want this feature to be low-friction and only "value added".

Far be it that I get in the middle of this thread (and the complexity WebAuthn has spawned), but if there is no intent to implement FIDO for its raison d'etre, why bother implementing it at all?

@lgarron
Copy link
Contributor Author

lgarron commented Feb 20, 2021

if there is no intent to implement FIDO for its raison d'etre, why bother implementing it at all?

I’m not sure I follow. We’re implemented “trusted devices” as described in the spec (see my link above).

The trusted device concept works regardless of whether a user has additional 2FA options, and we want to offer something that is both safer and more convenient than what the vast majority of users are currently using.

Industry-wide adoption numbers for 2FA are low, and I think that gating trusted device functionality behind the complexity of normal 2FA is counter to the goals and the spirit of the spec.

(That said, I think it would be great to get to the point where WebAuthn can be used so commonly that we can raise the bar for non-WebAuthn login as well.)

@Kieun
Copy link
Member

Kieun commented Feb 20, 2021

It seems that you tries to prompt "platform authenticator" when the user visits sign-in pages and he/she has already registered the "platform authenticator" with that device.
And also you don't want to prompt authentication with all the list of allowCredentials whether the registered credential is discoverable or not in a first place.

My understanding is that RP would rely on some "ambient credential" (with cookie or something) indicating the user registered the platform authenticator in that machine (and browser as well).

So, if there is any client side "ambient credential" when the user visits sign-in pages, you can prompt the webauthn authentication (depending on your policy, you may include that user's other credential).
In normal cases, the user goes through user verification (Touch Id or something) and you get authentication.
In some error cases, you might get "NotAllowedError" saying that the user might cancel the operation (user verification) or there i s no available authenticator with the credential ids (maybe swiped out).

In this case you can provide a button for the user to retry the webauthn authentication so that the user might retry or sign in with the password. (since you cannot distinguish "user cancel" and "not available authenticator")
If the user sign in with the password, you might ask re-registration of platform authenticator.

If there is no ambient credential, you'd better not asking webauthn authentication (trusted device) in a first place.

  1. The user needs to sign in with the password first
  2. You can determine whether the user has already registered the platform authenticatior with that device by asking additional webauthn authentication prompt (if there is any registered platform authenticator to that user).
    If the webauthn authentication is successful, you might create ambient credential for this browser (and device).
  3. If there is no platform authenticator credential for the user, you can ask for the user to register their platform authenticator.
    Then, you create the ambient credential for the trusted device feature.

That said you need to rely on the "ambient credential" when you prompt the "trusted device" authentication and you should create "ambient credential - hint" on the every browsers (when the user register their platform authenticator or authenticate with platform authenticator).

@lgarron
Copy link
Contributor Author

lgarron commented Feb 20, 2021

Indeed, therein lies the problem — getting a new browser profile into an appropriate "ambient credential" state.

Your first few paragraphs pretty much describe our current goals.

If there is no ambient credential, you'd better not asking webauthn authentication (trusted device) in a first place.

2. You can determine whether the user has already registered the platform authenticatior with that device by asking additional webauthn authentication prompt (if there is any registered platform authenticator to that user).
If the webauthn authentication is successful, you might create ambient credential for this browser (and device).

Unless I'm misunderstanding, these two are exactly at odds. If we follow step 2 as written, then we are asking the user for authentication at a time when there is no ambient credential. We can try to dress it up a friendly UI, but there is no way to control the browser UI for this, which is designed for authentication.

As mentioned before, we'd like to use heuristics to reduce how often this happens, by e.g. skipping straight to your step 3 if we don't think the user has an registered authenticator with the same platform scope (e.g. Windows Hello in any browser/Chrome on macOS/Safari on macOS/Safari on iOS). But our heuristics might be wrong, hence our desire to tell if we guessed wrong, i.e. when the new registration failed because of an existing registration.

@Kieun
Copy link
Member

Kieun commented Feb 22, 2021

I'm thinking that shared credential for the platform authenticator across browsers is similar concept to roaming authenticator, because the credential can be roaming between browsers.

You can guess if you get "InvalidStateError" when you try to create the credential for the platform authenticator. But, you never 100% confirm that the credential can be used for the authentication without asking authentication.
My guess is that you might create the "ambient credential" if you get "InvalidStateError" during create call.

Unless I'm misunderstanding, these two are exactly at odds. If we follow step 2 as written, then we are asking the user for authentication at a time when there is no ambient credential. We can try to dress it up a friendly UI, but there is no way to control the browser UI for this, which is designed for authentication.

After the user logs in with the password in case of no ambient credential on the browser, you could identify whether the user has registered the platform authenticator before or not.
If there is no platform authenticator registration, you might finish sign-in process or asking 2FA depending on the user's security settings.

If there is, you could ask WebAuthn authentication w/ allowList (also indicating internal transport as well), then browser will handle that request.

  • Depending on the browser, the browser might not show any UI and just return an error.

Then, you can explicitly know that the user can authenticate with the platform authenticator on that browser if the authentication is successful.

The other thought is that you just simply call create function without the exclude credential. Instead, you might need to use another attribute for ambient credential instead of generated credential id or something.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stat:Discuss subtype:Inquiry Inquiry wrt API and/or protocol capabilities/features, or WG / pub process, or spec editorial stuff
Projects
None yet
Development

No branches or pull requests

6 participants