Improving authentication with hardware keys / FIDO2 / webauthn + 2FA #9776

Open
opened 2025-11-02 08:49:16 -06:00 by GiteaMirror · 8 comments
Owner

Originally created by @noerw on GitHub (Nov 3, 2022).

The Problem


Possbile Solutions

(1) The Very Simple Fix

Revert #11573, but otherwise leave the auth conditions as they are. Fixes the UI inconsistency and the no-backup code situation, implements github's authentication logic, but does not enable passwordless login and makes phishable credentials mandatory.

(2) The Somewhat Simple Fix

  • Refactor the web UI: hardware key UI is moved to into the 2FA UI panel, so it's clear that they are not for passwordless auth.
  • Require either >=2 fido keys, or generate backup keys, when registering the first fido key. The login flow might also need changes to accept backup codes when no TOTP is registered.

This mostly keeps the current auth logic, except for requiring a backup second factor to be set up:

username && pw && (totp || fidokey1 || fidokey2 || backupcodes)

This auth logic does not require phishable credentials.

(3) The Correct Solution

The most correct way imho would be to implement the auth logic that is outlined below as Nextcloud's implementation.
Effectively the needed changes would be:

  • allow registering hardware keys as primary or secondary factor
  • migrate existing hardware keys to secondary factor keys
  • change auth logic of primary factor keys to actually be replacements for password auth

This would result in the following auth logic:

username && (pw || fidokey1 || fidokey2) && (totp || fidokey3 || backupcodes)

Of course not all of those credentials need to be used, all but one from each block could be unavailable/unknown. This option has the benefit to get along without mandatory phishable credentials (password, TOTP), as well as enabling passwordless auth.


Context

Gitea's current (as of 1.18.0+dev-558-g94d6d93cc, 94d6d93cc) authentication behaviour is as follows:

context auth requirements
default username && pw
TOTP 2FA enabled username && pw && (totp || backupkeys)
(multiple) fido2 keys registered username && pw && (fidokey1 || fidokey2)
TOTP 2FA enabled & fido2 keys registered username && pw && (fidokey1 || fidokey2 || totp || backupcodes)

For reference some other implementations of FIDO2:

implementation context auth requirements
github, mastodon default username && pw && emailed-token
TOTP 2FA enabled username && pw && (totp || backupkeys)
(multiple) fido2 keys registered case not allowed, must enable 2FA
TOTP 2FA enabled & fido2 keys registered username && pw && (fidokey1 || fidokey2 || totp || backupcodes)
nextcloud default username && pw
TOTP 2FA enabled username && pw && (totp || backupkeys)
(multiple) fido2 keys registered username && (pw || fidokey1 || fidokey2)
TOTP 2FA enabled & fido2 keys registered username && (pw || fidokey1 || fidokey2) && (totp || backupcodes)

As you can see, there is no clear correct de-facto implementation, I have yet to check fido2/webauthn spec if there is a correct approved way of implementing things. However there is a strong preference in the wider community on how to do things correctly with security and usability in mind:


some SEO optimization: webauthn u2f security key 2fa totp fido fido2
Originally created by @noerw on GitHub (Nov 3, 2022). ## The Problem - [ ] Adding a FIDO2 hardware key effectively / implicitly enables 2FA, but without providing a backup mechanism as with TOTP 2FA. This is potentially dangerous. - [ ] It can be argued that webauthn is supposed to be passwordless auth, enabling 2FA should be a separate choice, and that mandatory TOTP 2FA is reducing security instead of increasing it due to phishing/MITM risk. - #17495 - https://github.com/mastodon/mastodon/issues/16693 - [ ] The security settings page misleadingly implies 2FA is not enabled, while it effectively is: ![image](https://user-images.githubusercontent.com/7880552/199829706-7c9661c1-664e-48d5-8c87-5168dd237ff1.png) --- ## Possbile Solutions ### (1) The Very Simple Fix Revert #11573, but otherwise leave the auth conditions as they are. Fixes the UI inconsistency and the no-backup code situation, implements github's authentication logic, but does not enable passwordless login and makes phishable credentials mandatory. ### (2) The Somewhat Simple Fix - Refactor the web UI: hardware key UI is moved to into the 2FA UI panel, so it's clear that they are not for passwordless auth. - Require either >=2 fido keys, or generate backup keys, when registering the first fido key. The login flow might also need changes to accept backup codes when no TOTP is registered. This mostly keeps the current auth logic, except for requiring a backup second factor to be set up: ``` username && pw && (totp || fidokey1 || fidokey2 || backupcodes) ``` This auth logic does not require phishable credentials. ### (3) The Correct Solution The most correct way imho would be to implement the auth logic that is outlined below as Nextcloud's implementation. Effectively the needed changes would be: - allow registering hardware keys as primary *or* secondary factor - migrate existing hardware keys to secondary factor keys - change auth logic of primary factor keys to actually be replacements for password auth This would result in the following auth logic: ``` username && (pw || fidokey1 || fidokey2) && (totp || fidokey3 || backupcodes) ``` Of course not all of those credentials need to be used, all but one from each block could be unavailable/unknown. This option has the benefit to get along without mandatory phishable credentials (password, TOTP), as well as enabling passwordless auth. --- ## Context Gitea's current (as of 1.18.0+dev-558-g94d6d93cc, 94d6d93cc) authentication behaviour is as follows: | context | auth requirements | |--|--| | default | `username && pw` | | TOTP 2FA enabled | `username && pw && (totp \|\| backupkeys)` | | (multiple) fido2 keys registered | `username && pw && (fidokey1 \|\| fidokey2)` | | TOTP 2FA enabled & fido2 keys registered | `username && pw && (fidokey1 \|\| fidokey2 \|\| totp \|\| backupcodes)` | For reference some other implementations of FIDO2: | implementation | context | auth requirements | |--|--|--| | github, mastodon | default | ***`username && pw && emailed-token`*** | || TOTP 2FA enabled | `username && pw && (totp \|\| backupkeys)` | || (multiple) fido2 keys registered | ***case not allowed, must enable 2FA*** | || TOTP 2FA enabled & fido2 keys registered | `username && pw && (fidokey1 \|\| fidokey2 \|\| totp \|\| backupcodes)` | | nextcloud | default | `username && pw` | || TOTP 2FA enabled | `username && pw && (totp \|\| backupkeys)` | || (multiple) fido2 keys registered | ***`username && (pw \|\| fidokey1 \|\| fidokey2)`*** | || TOTP 2FA enabled & fido2 keys registered | ***`username && (pw \|\| fidokey1 \|\| fidokey2) && (totp \|\| backupcodes)`*** | As you can see, there is no clear correct de-facto implementation, I have yet to check fido2/webauthn spec if there is a ~~correct~~ approved way of implementing things. However there is a strong preference in the wider community on how to do things correctly with security and usability in mind: - on mastodon: https://github.com/mastodon/mastodon/issues/16693 - earlier on gitea: https://github.com/go-gitea/gitea/issues/5410 - on gitlab: https://gitlab.com/gitlab-org/gitlab-foss/-/issues/48918 --- <details>some SEO optimization: webauthn u2f security key 2fa totp fido fido2</details>
GiteaMirror added the type/proposaltopic/authenticationtopic/security labels 2025-11-02 08:49:16 -06:00
Author
Owner

@lunny commented on GitHub (Nov 4, 2022):

I would like the first implementation(github, mastodon), nextcloud's looks like a 3FA when TOTP 2FA enabled & fido2 keys registered

@lunny commented on GitHub (Nov 4, 2022): ~I would like the first implementation(github, mastodon), nextcloud's looks like a 3FA when TOTP 2FA enabled & fido2 keys registered~
Author
Owner

@noerw commented on GitHub (Nov 4, 2022):

nextcloud's looks like a 3FA when TOTP 2FA enabled & fido2 keys registered

If you look closely, you see that it's still 2FA: username && (pw || fidokey1 || fidokey2) && (totp || backupcodes).

The decision here is how much effort the gitea project is willing to put into changing this auth logic, with the possible gain of having an unphishable authentication method (to be clear, Gitea currently has this property (since #11573), so we should think hard about dropping it).
That said, I don't think touching this is urgent, as there is no acute security issue with the current way this auth is handled, the UI is just misleading currently. So it may be beneficial to wait and do this properly once the resources or priority for it are there, and refactor the UI in the meantime.

@noerw commented on GitHub (Nov 4, 2022): > nextcloud's looks like a 3FA when TOTP 2FA enabled & fido2 keys registered If you look closely, you see that it's still 2FA: `username && (pw || fidokey1 || fidokey2) && (totp || backupcodes)`. The decision here is how much effort the gitea project is willing to put into changing this auth logic, with the possible gain of having an unphishable authentication method (to be clear, Gitea currently has this property (since #11573), so we should think hard about dropping it). That said, I don't think touching this is urgent, as there is no acute security issue with the current way this auth is handled, the UI is just misleading currently. So it may be beneficial to wait and do this properly once the resources or priority for it are there, and refactor the UI in the meantime.
Author
Owner

@theAkito commented on GitHub (Dec 3, 2022):

Was about to create a dedicated issue, but found this one, when searching for existing ones, so I'll add another improvement.
If you need a separate issue out of this, I'll make one.

Gitea's WebAuthn does not save the domain string.

Information about the topic follows.

@theAkito commented on GitHub (Dec 3, 2022): Was about to create a dedicated issue, but found this one, when searching for existing ones, so I'll add another improvement. If you need a separate issue out of this, I'll make one. Gitea's WebAuthn does not save the domain string. Information about the topic follows. * https://stackoverflow.com/q/55090589 * https://stackoverflow.com/a/66267730 * https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get
Author
Owner

@lunny commented on GitHub (Mar 8, 2023):

Gitea's auth has a more complex situation. Some users want to login with a third auth server i.e. OAuth2 . I think we need to put all these situations together when refactoring/redesigning.

@lunny commented on GitHub (Mar 8, 2023): Gitea's auth has a more complex situation. Some users want to login with a third auth server i.e. OAuth2 . I think we need to put all these situations together when refactoring/redesigning.
Author
Owner

@f0sh commented on GitHub (Jul 19, 2023):

I would like to catch up on the statement of the beginning of this issue:

It can be argued that webauthn is supposed to be passwordless auth, enabling 2FA should be a separate choice, and that mandatory TOTP 2FA is reducing security instead of increasing it due to phishing/MITM risk.

Webauthn was designed as a generic, phishing proof protocol and therefore can be used for passwordless authentication. It brings it's own function of a 2nd factor (where you can use a 2F to access your local public key) but is also downward compatible as a 2F with U2F. That's a bit confusing. Because the domain is (as the relying party RP) fundamental part of the authentication process in webauthn, there is a drastically reduced attack vector for phishing in webauthn compared to U2F, because you just cannot use your credentials on a different site, without tampering your local machine.

When it comes to the implementation, then you actually do not use (or strictly saying, you do not have to use) a username anymore, because you are working with a publickey authentication approach.

So I totally agree with what @lunny said, that

I think we need to put all these situations together when refactoring/redesigning.

because there would be actually a pool of equal authentication methods/backends, every with their own set of credentials:

  • classic username + password [+ 2FA]
  • publickey authentication (using Webauthn+FIDO2 / Passkeys)
  • API authentication (like OAuth or OpenID Connect)

This is indeed a lot of effort to implement. However, considering the move of bigtechs towards Passkeys and being already available on most platforms, this is worth the discussion and integration. Github just announced few days ago, that they now support passkey authentification as well. Paypal, Google, Apple are alreading doing it.

My point is, when tackling the described issues, I would suggest to refactor it in a way that the whole picture is considered as it is discussed in #24821. I think this will give more momentum. A first approach can be hanko.io.

@f0sh commented on GitHub (Jul 19, 2023): I would like to catch up on the statement of the beginning of this issue: > It can be argued that webauthn is supposed to be passwordless auth, enabling 2FA should be a separate choice, and that mandatory TOTP 2FA is reducing security instead of increasing it due to phishing/MITM risk. **Webauthn** was designed as a *generic, phishing proof protocol* and therefore can be used for *passwordless authentication*. It brings it's own function of a 2nd factor (where you can use a 2F to access your local public key) but is also downward compatible as a 2F with **U2F**. That's a bit confusing. Because the domain is (as the relying party RP) fundamental part of the [authentication process in webauthn](https://www.w3.org/TR/webauthn/#sctn-sample-authentication), there is a drastically reduced attack vector for phishing in webauthn compared to U2F, because you just cannot use your credentials on a different site, without tampering your local machine. When it comes to the implementation, then you actually do not use (or strictly saying, you do not have to use) a username anymore, because you are working with a [publickey authentication approach](https://www.w3.org/TR/webauthn/#iface-pkcredential). So I totally agree with what @lunny said, that > I think we need to put all these situations together when refactoring/redesigning. because there would be actually a pool of equal authentication methods/backends, every with their own set of credentials: - classic username + password [+ 2FA] - publickey authentication (using Webauthn+FIDO2 / Passkeys) - API authentication (like OAuth or OpenID Connect) This is indeed a lot of effort to implement. However, considering the move of bigtechs towards Passkeys and being already available on most platforms, this is worth the discussion and integration. Github just [announced](https://docs.github.com/en/authentication/authenticating-with-a-passkey/about-passkeys) few days ago, that they now support passkey authentification as well. Paypal, Google, Apple are alreading doing it. My point is, when tackling the described issues, I would suggest to refactor it in a way that the whole picture is considered as it is discussed in #24821. I think this will give more momentum. A first approach can be [hanko.io](https://github.com/teamhanko/hanko).
Author
Owner

@ptman commented on GitHub (May 20, 2025):

Seems like passkeys / webauthn is registered without verification required. So I'm able to login using my yubikey by just plugging it in and pressing the button WITHOUT entering my pin. So this makes it a 1FA instead of 2FA. Posession of yubikey is enough, without need to know pin. (I don't have a biometric yubikey)

0534eddd16/routers/web/user/setting/security/webauthn.go (L56)

https://pkg.go.dev/github.com/go-webauthn/webauthn@v0.13.0/protocol#UserVerificationRequirement

@ptman commented on GitHub (May 20, 2025): Seems like passkeys / webauthn is registered without verification required. So I'm able to login using my yubikey by just plugging it in and pressing the button WITHOUT entering my pin. So this makes it a 1FA instead of 2FA. Posession of yubikey is enough, without need to know pin. (I don't have a biometric yubikey) https://github.com/go-gitea/gitea/blob/0534eddd16a604fabfec464e36458e540244382d/routers/web/user/setting/security/webauthn.go#L56 https://pkg.go.dev/github.com/go-webauthn/webauthn@v0.13.0/protocol#UserVerificationRequirement
Author
Owner

@iddm commented on GitHub (Jun 28, 2025):

Seems like passkeys / webauthn is registered without verification required. So I'm able to login using my yubikey by just plugging it in and pressing the button WITHOUT entering my pin. So this makes it a 1FA instead of 2FA. Posession of yubikey is enough, without need to know pin. (I don't have a biometric yubikey)

gitea/routers/web/user/setting/security/webauthn.go

Line 56 in 0534edd

credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration(webAuthnUser, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
https://pkg.go.dev/github.com/go-webauthn/webauthn@v0.13.0/protocol#UserVerificationRequirement

Isn't the first factor your password? So you must know your user's login and password, AND have access to the smart card associated with his account.

@iddm commented on GitHub (Jun 28, 2025): > Seems like passkeys / webauthn is registered without verification required. So I'm able to login using my yubikey by just plugging it in and pressing the button WITHOUT entering my pin. So this makes it a 1FA instead of 2FA. Posession of yubikey is enough, without need to know pin. (I don't have a biometric yubikey) > > [gitea/routers/web/user/setting/security/webauthn.go](https://github.com/go-gitea/gitea/blob/0534eddd16a604fabfec464e36458e540244382d/routers/web/user/setting/security/webauthn.go#L56) > > Line 56 in [0534edd](/go-gitea/gitea/commit/0534eddd16a604fabfec464e36458e540244382d) > > credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration(webAuthnUser, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ > https://pkg.go.dev/github.com/go-webauthn/webauthn@v0.13.0/protocol#UserVerificationRequirement Isn't the first factor your password? So you must know your user's login and password, AND have access to the smart card associated with his account.
Author
Owner

@ptman commented on GitHub (Jul 2, 2025):

Isn't the first factor your password? So you must know your user's login and password, AND have access to the smart card associated with his account.

I'm not asked for a password. Passkeys are supposed to allow passwordless login, but in those cases the authenticator should do some sort of check (face, fingerprint, PIN, ...)

@ptman commented on GitHub (Jul 2, 2025): > Isn't the first factor your password? So you must know your user's login and password, AND have access to the smart card associated with his account. I'm not asked for a password. Passkeys are supposed to allow passwordless login, but in those cases the authenticator should do some sort of check (face, fingerprint, PIN, ...)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/gitea#9776