This article covers how to add “passwordless” authentication to any Remix application using WebAuthN. This allows users to authenticate using their iPhones FaceID, TouchID, a physical security key like a YubiKey, or a virtual Passkey that can be stored in password managers like LastPass/Bitwarden/OnePassword etc..

“Passkeys are like passwords but better. They’re better because they aren’t created insecurely by humans and because they use public key cryptography to create much more secure experiences.”

- https://passkey.org

Why integrate with WebAuthN? (ELI5)

WebAuthN is a more secure and usually a better UX for users to authenticate with your application. Instead of typing a password, or opening a password manager and finding your login credentials, one can just use their phone’s native bio authentication mechanisms like FaceID or fingerprints to authenticate.

This involves creating a pair of private and public keys and storing the private keys on a local device, and sharing the public keys with a server’s database. This process is facilitated by WebAuthN standards.

“Although there are many ways to do MFA, security keys are a highly secure and convenient option. Login codes sent via text message can be intercepted, and authentication apps require that your phone be charged and available.”

- https://www.yubico.com

Even if the database’s data is compromised and data is leaked, it will not matter because the public key does not have any important information - it is only useful when paired with the private key.

Adding this feature to your application allows extra security by enforcing it after a normal login for an MFA flow, or just to replace the password altogether. It can also be considered a faster MFA implementation because it does not require waiting for a text message or email with a code to copy/paste, or an Authenticator app to copy/paste 6-digit codes from.

How to integrate WebAuthN into Remix

The process involves two steps. First step is to have users register some credentials, the second step is to use the credentials when the user tries to login in the future and verify their identity.

This process can be achieved completely “natively” by using the Browsers navigator.credentials API for creating a getting keys on the client side, but it can be more tedious overall since it is more low level.

So this article will use the great library SimpleWebAuthN from Matthew Miller, which provides packages for the server side, client side, and typescript types, that make everything easier.

Registering Credentials

For both registration and authentication of credentials, the flow is similar. For registration it goes like this:

  1. The client indicates to the server that it wants to register some new credentials.
  2. The server generates some options that it will pass back to the client, which includes a secret challenge. This is done in the library with webauthn.generateRegistrationOptions(...). The secret challenge is stored in a readonly http session cookie.
  3. The client takes the options and passes it to the WebAuthN facilitator, such as SimpleWebAuthN library to do webauthn.startRegistration(options) - the client will be prompted by the browser or native OS to choose a format, such as FaceID or passkey.
  4. The client then takes the result of the registration and passes it back to the server.
  5. The server validates and verifies the results with something like webauthn.verifyRegistrationResponse(results) which includes the secret challenge from the session.
  6. If the verification passes, the publicKey is stored in the database, along with the corresponding credentialID, the transports and a counter. The transports just indicate the type of device that was used, such as a USB key or other means, and the counter is used to ensure the same device is not used too many times. This data will be fetched and used when the user tries to login later.

Side note: When generating the options on the server - the server can include already existing keys for that user in the options response to prevent the client from trying to register the same credential twice.

Overall, there are a few back-and-forth requests between server and client, and in Remix this can be handled in a few different ways, either using native Forms or by using Remix’s useFetcher hooks, or just with normal http ajax requests like Fetch, Axios or the O.G XMLHTTPRequest.

Session cookies are stored using Remix’s createCookieSessionStorage().

Authenticating

After the credentials are created and stored on the user’s device and the public key is stored in the app’s database, we can now use them to authenticate the user.

The flow is as follows (for passwordless):

  1. The user indicates to the server that they would like to login.
  2. The server generates some options that it will pass back to the client, which includes a secret challenge. This is done in the library with webauthn.generateAuthenticationOptions(...). The secret challenge is stored in a readonly http session cookie.
  3. The client takes the options and passes them to webauthn.startAuthentication(options) - the client will be prompted by the browser or native OS to choose a format, such as FaceID or passkey.
  4. The client then takes the result of the registration and passes it back to the server.
  5. The server validates and verifies the results with webauthn.verifyAuthenticationResponse(results) which includes the secret challenge from the session.
  6. If the verification passes, the user is considered valid and another session cookie is set, such as isLoggedIn and is set to true, with an expiry date for however long you want the user to be authenticated without having to re-authenticate. The server can redirect the user to an authenticated page with the new session cookie so they can use the app freely, and authenticated.

Recovery

Overall WebAuthN is a great solution for stellar UX and strong security in applications - but a drawback can be if the user loses their security device, or loses their phone which holds their private keys. This would mean they lose access to any application that requires their private key for authentication on the client side.

That’s why it is important to also provide a recovery mechanism for lost devices. Most applications provide a series of “codes” that the user should store somewhere else, in case they lose access to their primary device, they can add a new device using the recovery codes.

Conclusion

That’s it! It is pretty simple to integrate WebAuthN into Remix applications, and it provides great UX and great security for users.

For further information about WebAuthN, check out their official site webauthn.io!

Follow me on twitter (@Rametta) to hear about new articles.