2

I'm thinking about adding support for WebAuthn / passkeys to my web app, but the fact that you need to have separate register and sign-in flows, and usernames are still required, make it pretty much a no-go for me. And I am really wondering if I am missing something here, or if this can be made invisible to the user somehow.

For example I am currently offering sign-in via email: the user simply enters their email address, and receives a sign-in link. No matter if they are an existing user or not; I can simply create a new account if it's a new email address. To the user this is completely invisible, they don't have to choose between a "register" and "sign in" button, they don't have to try and remember if they already have an account or not. Just enter your email address and that's it.

I am also offering Sign In with Apple which has the same flow: I get a unique Apple user ID (subject) in the token from Apple, check if I have a user with this ID in the database, and log that user in. And if not, I create a new user, store that ID, and log them in. Once again, the user never has to choose between "register" and "sign in", they don't have to enter any personal information at all.

But with WebAuthn you have to create or get the credentials with a username. So I'd have to show a username input field on the website, send that to the server, the server checks if that user exists, and based on that the website can either create or get the credentials. But usernames suck, everybody having to choose a unique one is always frustrating once you have enough users. So instead I could ask for an email address but I want to store as little personal identifiable information as possible (yes, I offer the sign in via email method, but that's why I want to offer WebAuthn as an alternative option).

So, how can this be streamlined? Is there a way around the username requirement, where the browser simply asks the authenticator if they have a public key for this website, and if not, create it. That way I can offer a single "sign in with passkey" button on the website without the need to ask for a username (or email address).

Kevin Renskers
  • 5,156
  • 4
  • 47
  • 95
  • Hey Kevin, we've been working on this for quite a while and tried many different approaches. There are many things to consider, most notably account recovery and the ability to sign in on different devices (platforms, really). You can see our current iteration here: https://example.hanko.io – FlxMgdnz Sep 01 '22 at 08:50
  • That seems to suffer the exact problem that I am trying to solve: as a first time user pressing on the "Sign in with passkey" button, it doesn't work. I think that is bad UX - I don't want to have two separate register and sign in flows, so to speak. – Kevin Renskers Sep 01 '22 at 09:17
  • Fully agree. The passkey button is just a temporary solution until Conditional UI is supported everywhere. It's sort of a passkey autofill you can place on the username field. We're almost done with our integration of it. No need for the passkey button anymore then. iOS 16 and macOS Ventura will ship with support for Conditional UI in a few days, the others will follow over the coming months. – FlxMgdnz Sep 01 '22 at 09:30
  • But if you only want a single "sign in with passkey" button, that is still very much not possible. – Kevin Renskers Sep 01 '22 at 09:33
  • 2
    I get what you're saying. Passkeys in their current state are designed as a password replacement, not to replace OAuth logins. That's why we're happy to get rid of the passkey button as soon as possible – because it implies something that it's not. – FlxMgdnz Sep 01 '22 at 12:04
  • feel free to join the discussion: https://github.com/w3c/webauthn/issues/1942 – r j Aug 19 '23 at 08:19

4 Answers4

2

While you technically don't need a username for discoverable WebAuthn credentials (passkeys), you still need a human recognizable string for the user to recognize for the credential.

For example, if the user has two different accounts for a site, you don't want them seeing a random string when selecting an account from the passkey credential list, as that won't be helpful for the user to select the proper credential.

Tim
  • 827
  • 4
  • 6
  • But even a random string wouldn't work, would it? The browser still has to know if it has to use the create or get method, and the only way to know that, is to check on the server if the account already exists? So if every time you open the website you get a new random string, you'd end up with a new account every time. – Kevin Renskers Aug 31 '22 at 21:47
  • Also, how would you deal with people using the website on multiple browsers or devices? Phone, tablet, computer, library, whatever. I guess that's why the username is required and why it has to be known by the user.. but it just sucks that the user still has to choose and remember a unique username.. it feels like WebAuthn only solves half of the problem. – Kevin Renskers Aug 31 '22 at 21:49
  • No, this is why passkeys are discoverable credentials, meaning you don't need any initial knowledge server-side to use them. – Tim Aug 31 '22 at 21:50
  • Can you elaborate on that? I am not sure I understand what you mean. – Kevin Renskers Aug 31 '22 at 21:51
  • Not sure what you mean with your second question. There is no difference across browsers or devices. Have you seen the demo video? https://youtu.be/SWocv4BhCNg?t=102 – Tim Aug 31 '22 at 21:52
  • The user clicks your sign in button, you invoke WebAuthn without specifying any credentials in the allow list. The user is presented with the system dialog. If they have a single passkey, they will just be prompted for user verification. If they have multiple passkeys, they will be prompted to select one. We've also added some functionality that will show an autofill-style UI in the username field if there are passkeys on the device for your site. – Tim Aug 31 '22 at 21:53
  • Right so in that video he first signs in with a regular username + password account, then gets asked to store a passkey. But I'd like to skip that step. And I can't just call `navigator.credentials.get` without the `allowCredentials` list, without also first actually calling `navigator.credentials.create` at some point. And therein lies the problem, right? How do I know which of the two methods to call? – Kevin Renskers Aug 31 '22 at 22:02
  • get is for signing a user in with a WebAuthn credential (passkey) - e.g. the user clicks "sign in with a passkey" or clicks into a username field and the autofill UI presents a credential. create is for creating a new passkey - e.g. during an account creation process or when the user goes into their profile and wants to add another passkey – Tim Sep 01 '22 at 00:36
1

So, how can this be streamlined? Is there a way around the username requirement, where the browser simply asks the authenticator if they have a public key for this website, and if not, create it. That way I can offer a single "sign in with passkey" button on the website without the need to ask for a username (or email address).

If you want to fully commit to "usernameless" use of WebAuthn, where the user doesn't need to provide any information prior to creating an account, then just make something up for the three required values in user that you pass to navigator.credentials.get():

const credential = await navigator.credentials.create({
  publicKey: {
    // ...
    user: {
      id: randomUserIDBytesGeneratedByServer,
      name: bytesToString(randomUserIDBytesGeneratedByServer),
      displayName: bytesToString(randomUserIDBytesGeneratedByServer),
    },
    authenticatorSelection: {
      residentKey: 'required',
      userVerification: true
    },
  },
});

randomUserIDBytesGeneratedByServer is some random, unique ID that you generate upon a new user clicking your Register button. Provide some kind of encoded value for name and displayName (I use bytesToString() as a shorthand for something you do to encode those random bytes into some kind of string, for example to base64url).

Send the discoverable credential back to your server, verify it, and upon successful completion create in your database a new "User" account that gets assigned randomBytesGeneratedByServer as its ID. Make sure to also associate credential.id to randomUserIDBytesGeneratedByServer in another table for later.

When the user returns to log in, have them click an Authenticate button to trigger navigator.credentials.get() with an empty allowCredentials array. This will enable the user to select the discoverable credential that was registered earlier:

const credential = await navigator.credentials.get({
  publicKey: {
    // ...
    allowCredentials: [],
  },
});

Upon successful verification of the response returned from the call to .get(), you can look up the User's ID by grabbing randomUserIDBytesGeneratedByServer from the table that you used to remember which user credential.id belonged to. At that point you can establish an authenticated session for your user by whatever means is appropriate to your setup.

This might sound a bit extreme, but in truth there's nothing about WebAuthn that prevents you from requesting credentials and allowing authentication without gathering any user-identifiable information of any kind.

IAmKale
  • 3,146
  • 1
  • 25
  • 46
  • Of course it's worth noting that there aren't any mechanisms by which to update a discoverable credential's display name within the authenticator. If you allow users to choose a username after registration, you'll need to perform another registration with the username as `name` and `displayName`, but same `randomUserIDBytesGeneratedByServer` as the `id` so that the authenticator overwrites the first credential with a new one using the user's actual username. – IAmKale Sep 01 '22 at 01:39
  • 2
    And an additional caveat: unless you go through those extra steps after registration to allow the user to pick a username, users will be very confused later when they go to log in and their authenticator presents to them a list of nonsensical base64url strings to choose from. This is typically why you have people choose a username beforehand. – IAmKale Sep 01 '22 at 01:45
  • My app doesn't have usernames at all, and even email addresses are optional. When they choose to Sign In with Apple for example, I don't have any personal identifiable information at all, which is just perfect for privacy reasons. But I really don't want to have separate register and sign in buttons. – Kevin Renskers Sep 01 '22 at 09:20
1

Non-discoverable credentials require the relying party website to provide a list of previously registered credential handles. From this, the local client will ask any authenticators if they understand these credential handles. If so, the user is able to use that authenticator, and release an authentication method signed with the public key associated with that credential handle.

This is typically used for two-factor authentication, where some primary factor (such as username and password, or a prior authentication session), is used to look up potential credential handles.

There are sites which will ask for a username, then return credential handles and use non-discoverable credentials for primary factor, but there are disadvantages with this:

  1. The prior specification for how authenticators work, U2F, did not support any additional user verification. So this sort of authentication would only provide a possession factor - someone who knows you can take your yubikey and use it to get into these services.

  2. Since you are releasing credential handles on entry of a username/email without any initial authentication, you leak information about the user - that they have an account with that email, that they are using WebAuthn, how many authenticators are registered to their account, and so on.

To contrast, Discoverable credentials can be used without providing a credential handle. This allows for these 'usernameless' flows - when triggered, the relying party simply asks 'do you have any credentials I can use', and the client asks authenticators what they have stored which is appropriate.

The disadvantages of this approach are availability:

  1. older U2F hardware keys do not support discoverable credentials

  2. modern hardware keys which support discoverable credentials may have limited flash storage for them.

However, passkeys being integrated into platforms with comparatively limitless storage will change the availability equation soon.

I would not recommend @IAmKale's approach of using random values for a discoverable credential. For one, users may very well be presented the name and displayName values, and a garbage string will not be a good user experience.

Secondly, you are assured that you can register a credential with the same user id to overwrite the existing credential. You do not, however, have any way to delete credentials previously registered with your site. Giving a different randomly generated value on each registration might cause the user to be presented with a choice of credentials, even if only one of those credentials will lead to a successful login. Even if the client or platform has a way for the user to delete spurious entries, this isn't a great experience.

Instead, I would recommend:

  • however you identify the user, associate and persist a new random value for a webauthn user id
  • when you register a credential for that user, supply any clarifying information you might have (such as their contact email) along with that persisted identifier
David Waite
  • 104
  • 3
  • Discoverable credentials definitely sound like the way to go, but the problem is still that the user first has to sign in via another method, to then generate and register a passkey. You can't really offer just a single "sign in with passkey" button on a website, the way that you CAN do with Sign In with Apple. There is no separate register step needed there. The problem with a "sign in with passkey" button is that it simply doesn't work unless the user has gone through a register step. – Kevin Renskers Sep 01 '22 at 09:26
  • 1
    Correct, passkeys are credentials that can be linked to an account. They do not aim to solve account creation (technically neither does federated sign in as you still need to do account creation logic on the backend), but it drastically simplifies it. You could continue to just use SIwA, but you're locking users out of your service and there are privacy implications for third party federation. – Tim Sep 01 '22 at 12:48
  • Couldn't you just attempt login, then detect/catch if that fails (e.g. no available credentials for this site) and launch the registration flow with discoverable credentials required and server generating a new user ID? Since you have everything open, it wouldn't be an authenticated identity based on anything other than the keypair, but I think that is what OP is asking for. Kinda like a glorified tracking cookie but one that doesn't expire. – Phil Jul 24 '23 at 00:44
1

There are 2 separate things.

  1. Federated vs. Webauthn

Webauthn is not federated credential so you cannot just outsource them authentication like to an email provider or to apple, google etc. If you want to do that, you need to do that but you are already doing it and want something different, I guess? A had the feeling you want to get rid of federated but you want to keep outsourcing authentication, which is: federated (have the cake and eat it too or the other way around).

By federated your user is the owner of the email account or of the apple, google etc. account. No need to register because your user registered elsewhere. You get an id and an authentication service from others...

If you want webauthn, you need to authenticate yourself and register your user with create() first. On the server you store internal uid -> pid (passkey id which is called user id and user handle in the webauthn specs but it is actually a passkey id) and pid->public key. On the client side the pass manager (authenticator) stores: xyz.com -> {pid, private key} and sync it. I will call passkey the private key... some use it for the key pair.

Your users anonymous identity is the pid or rather a set of pids since it seems to go to the direction of creating more than one passkeys per account instead of solving the syncing of one passkey... you verify the identity or the pid with the verification key which is the public key.

By a sign in process get() is called and a random dynamic server challenge is signed by the pass manager with the private key and you verify the signature and hence the pid as identity with the corresponding public key on your server.

It is very nice math and anonymous and fishing resistant etc. but it is you(!) that verifies the identity and not someone else! So first you have to create it, even worse: as of now on apple, google, microsoft and later on linux platforms separate ones (a set of pids, a set of public keys).

I am not sure it is not the very thing that will bury webauthn passkeys since it will be a maintenance hell for users/website owners in the long run. A security problem too since plenty of users will start to create hordes of passkeys without ever used a pass manager before or without knowing that they actually use one... Webauthn passkeys require a pass manager and there are people who do not want to use one or will not know they use one... it might work in the beginning but it implies that people will all be able to handle a pass manager. I am not sure of that...

  1. Usernameless internal logic with 2 required usernames

I think you are correct and it is cool you do not use usernames and email address is an option. The sad thing is, webauthn is logically usernameless (pid is the real anonymous identity and pass manager use the pid, the 2 usernames are just labels in case more than one account per domain) but the webauthn spec has a logical misstep by requiring not just one but 2 username fields... I personally think it is very bad and at least they should make these fields optional. The default thinking should be that one real person has one account per domain and then the whole UX would be super nice and simple: "create passkey for xyz.com" and "use your passkey for xyz.com" (one entry per domain per pass manager without any usernames, with simplified UX with less unnecessary text).

If someone creates more than one account which is possible, this person has the responsibility to manage pass manager entry labels and differentiate the accounts (one optional note field instead of 2 username fields would suffice...). It is not common thinking but extremely logical :) Webauthn should totally abolish username fields since they added pid (user id, user handle - they manage to call it 2 different things). It is not the web site owners responsibility to micromanage pass manager labels in the users pass manager. The conflict of having 2 accounts per domain is actually a misgeburt and created by the user and it is a design smell (like websites using emails as usernames and users want a burner account too which they would not want in the first place if accounts that do not require real identity would stop gathering personally identifiable information).

More about this topic here: User name and displayName change for existing passkey

All in all, webauthn is not federated, it is you who has to check the identity by "sign in" using get() and for this you need a first step to register an anonymous identity via create().

With the username mess I think you have the right feeling, it should not be there.

r j
  • 186
  • 8