Simple Two-Factor Authentication

Please note: This article is only available in English.
Two-factor authentication should be part of every public account system, especially since it can be implemented very easily.

I know I haven't written a post in a while, so maybe I am going to change this soon again. Hopefully, this will only be the first of several posts on this and other subjects worth placing on the Internet.

In the recent weeks I've been busy activating two-factor authentication (2FA) on every web platform that I am actively using. Especially the ones where money is in the play (eCommerce, finance, insurance, cloud providers, ...) needed an extra safety net. After all, we are "never safe from being pwned" (to quote Troy Hunt). While I was going for the classic SMS / Text 2FA at first I pivoted soon after to using an authenticator app. The two most prominent ones are Google Authenticator and Microsoft Authenticator. For some reasons I've picked the latter, even though the former seems to have a better reputation.

In this post I don't want to compare the various authenticator apps, or go into implementation details on their side. I want to illustrate how easy it is to include 2FA in any application (web based or not) using these apps for the end-user.

The mechanism we will implement is called TOTP (short for time-based one-time password). This is an algorithm that provides a stateless 2FA mechanism, i.e., a mechanism where the verification is based purely on a (hopefully securely) exchanged secret. The secret is then used in conjunction with the time to retrieve a slice of an HMAC. The size of the slice and the validation time window can be configured (6 and 30s are default values). Alternatively, there is also HMAC-based One-time Password algorithm (HOTP), which is slightly different but follows the same key principle.

For a proper 2FA implementation of TOTP we need three things:

  • A way of generating a secret for the user (could be as simple as a GUID, but the more cryptographically secure the secret is the better)
  • A way of displaying a QR code to the user that contains the secret / TOTP URL (this will / should only be displayed once after enabling the 2FA)
  • A way of validating user input with potential time tolerances (i.e., if the code is refreshed every 30 seconds we should allow, e.g., 1 past and 1 future code)

While the first steps may be done without any library the third one could be more difficult (as this one requires following a specification and understanding tolerance levels, different hashing algorithms and more). Various libraries exist, but for this example I've chosen OtpSharp to keep things simple.

Alright, so let's say a user "activates" 2FA in our ASP.NET application. What should we do? We generate a secret for this user. It should be generated otherwise one could always / easily derive it. Also note that this secret should ideally be in a different spot than the hashed password, or at least be encrypted using a mechanism such that an attacker obtaining data from the DB would only have one piece for logging in (e.g., the hashed password), but missing the other piece. We'll come to the exact reason for that later.

The secret could be as simple as

Convert.ToBase64String(Guid.NewGuid().ToByteArray())

but keep in mind that we need to securely store it. For now let's just say that this task is beyond this post or trivial enough to skip it.

Now that we've generated a secret we need a good way of giving it to the user. Ideally, we place a short description that the following image is being used in the known authenticator apps (e.g., Google Authenticator). After the description we place an image containing a QR code as described here.

Important is that the QR code is only display after the 2FA was enabled. Since the QR code contains the TOTP URL, which contains the secret in plain text, showing it later increases the security risk of leaking the key to an unwanted party.

Generating the QR code can be done purely in the frontend (and should not go via third-party QR generator services - for the same reason that we should never share the URL / key with another party) via some JS libraries, e.g., qrcode.js.

Finally, the last step may be the most difficult one. We can make it much simpler by using a library. Let's say we just created an TOTP URL in the format

var urlToShare = $"otpauth://totp/{issuer}:{user}?secret={secret}&issuer={issuer}",

we can use the same URL to derive a verification test for an inserted code:

var totp = (Totp)KeyUrl.FromUrl(urlToShare);
return totp.VerifyTotp(code, out long _, new VerificationWindow(1, 1));

In the previously shown code we use a tolerance of 1 code from the future and 1 code from the past. This way we can play with time shifts coming either from our server (bad luck, we should work on that) or our users (their phones / devices may be off by some seconds, too). Furthermore, such tolerances make the input more user friendly (e.g., tolerating some input processing times and network latencies). The cost is that we artificially make the time window larger, which reduces security.

So is this a big step forward? Yes, it can be, but we need to make sure to really securely store this generated secret key. In the end the problem with stateless OTOP 2FA is that clones of the code (i.e., having the URL / key written down somewhere) are indistinguishable. Hence we always must trust users that they only copied / inserted the code once and that their trusted apps work securely (these apps also must store the OTOP URL, which contains the secret).

Nevertheless, handled correctly by all parties, the site provider, the authenticator app, and the user, we see that this way 2FA is implemented rather easily without much compromise in terms of convenience. No more waiting for SMS, having to carry around TAN lists, or need for accompanying physical devices like security tokens.

Created .

References

Sharing is caring!