Users can now add Security Keys as second factor authentication method via the Webauthn API. RFC on Meta: Webauthn support - feature - Discourse Meta.
Users can register security keys from the Second Factor Authentication preferences screen. They can give each key a name and edit the name & disable a key if required. When they register the key they are prompted to insert and use their physical key:
When the user logs in and they have active security keys, they are shown a prompt to authenticate using a security key. When they click “Authenticate With Security Key” they are prompted to use their physical key. In the case of having multiple keys registered, we just tell the webauthn API all of the allowed credential IDs for the user, and the first key matching any of them is used when logging in.
If a user does not have their security key with them they can choose to fall back to TOTP based 2FA.
The missing case I’ve found is “What happens when a user has only security keys registered and loses all of them?”. This seems to happen as well when I only set up one TOTP code and no backup codes, I cannot proceed in login if I have “lost” my code. What happens normally here?:
- I have used
Discourse.current_hostnamefor the Relaying Party ID (Web Authentication: An API for accessing Public Key Credentials - Level 2) which should be the “effective domain” of the relaying party. In localhost this is just
localhostand I assume in production sites it would be
- I’ve left out certain validation steps that are either optional or unnecessary for an initial implementation, e.g.
clientExtensionResults, fully verifying the attestation statement using trust anchors (using attestation format of
nonefor now), checking
tokenBinding.status. This does not reduce the security of the implementation.
- I’ve just used a single supported public key algorithm for now –
-7which is ECDSA w/ SHA-256. CBOR Object Signing and Encryption (COSE)
- I have only tested the implementation with Yubikeys; I do not have an Android phone / macOS / any other physical hardware authentication methods
- base64js.min.js - Used for converting base64 to and from bytes, for use with webauthn APIs
- cbor (gem) - webauthn credential attestation data is CBOR encoded, and we need to decode it to inspect the attestation
- cose (gem) - the public key for the webauthn credential is encoded in the COSE key format
- Added a
UserSecurityKeymodel and database table. The information stored for security keys differs enough from the
UserSecondFactortable that they need to be stored on their own. Also, at some point security keys will be used for first + multi factor auth.
Userwhich is only written when it is needed. This was done to satisfy
webauthn.credentials.create()by providing a random string to identify a Discourse user. Web Authentication: An API for accessing Public Key Credentials - Level 2. Using the user ID integer worked OK on an older Yubikey, but did not work at all on the YubiKey 5 NFC, so I think it’s safer to go with a random string for this.
- The code currently is feature complete, however controller tests for the session controller + user controller as well as a QUnit integration spec is still missing.