Multi Factor Authentication
Source: MS Docs
Multi-factor authentication (MFA) is a process in which a user is requested during a sign-in event for additional forms of identification. This prompt could be to enter a code from a cellphone, use a FIDO2 key, or to provide a fingerprint scan. When you require a second form of authentication, security is enhanced. The additional factor isn’t easily obtained or duplicated by a cyberattacker.
MFA requires at least two or more types of proof for an identity like something you know, something you possess, or biometric validation for the user to authenticate.
Two-factor authentication (2FA) is like a subset of MFA, but the difference being that MFA can require two or more factors to prove the identity.
Small note: 2FA is supported by default when using ASP.NET Core Identity. To enable
or disable 2FA for a specific user, set the IdentityUser<TKey>.TwoFactorEnabled
property.
Inside of MFA, there’s TOTP, (Time-based One-time Password Algorithm). Compliant apps such as Microsoft Authenticator and Google Authenticator can be tied to any implementation following said algorithm.
2FA and TOTP
Just as a preamble, we have RFCs with all details when it comes to how these algorithms should work and their considerations: RFC 6238 and RFC 4226.
In short we have to generate a key, (as per OWASP Recommendations these should be
random, could even be put on a rotation, and should also use strong hashing algorithms,
such as SHA-512). And with this key we can then generate a URL that could be encoded
to a QR. Said URL should follow the otpauth://
URL format. That would be a standard.
And in there through query params, we could be passing metadata that can then the application
use as inputs. Once it saves this TOTP, it should be generating codes in time that we
can then validate on the backend side with the same key we used.
Project Rundown
This is another ASP.NET Restful API, it focuses on the leverage of two libraries
QRCoder
and Otp.NET
. One will make sure of generating all the neccesary pieces
of information neccesary for the TOTP algorithm to take place. And the other one
will be focused entirely on generating a QR Code that can be then read by an Auth
App, and then start generating on its own a TOTP.
At a physical level the folder structure follows the solution structure:
- TotpPrototype
|-- src
| |-- TotpPrototype.Application
| | |-- TotpFunctions.cs
| | |-- Program.cs
|-- test
We don’t need much else in here since it’s pretty self-contained, the endpoints are as follows:
- /api/generate-key
-
This endpoint focuses solely on the usage of the
OtpNet
, it will generate a random key (secret) at byte level, and it will then encode those bytes into base32 so that we can print a string. - /api/generate-qr
-
This endpoint receives one request param under
secret=<KEY>
, and it will then try to save locally a QR that an Authentication App can scan and add to its vault. - /api/validate-code
-
This endpoint will receive the TOTP sent through a request param
totp=<CODE>
, and the key to validate it with throughsecret=<SECRET>
in base32. It will then in that specific point in time attempt to return if the code is valid or not.
Cross-Cutting Concerns
Security
The Base32Key
is a shared secret between the server and the user’s 2FA app. If someone
else obtains this key, they can generate valid TOTP codes for that user. To
mitigate this risk:
-
Only display the QR code temporarily. Once the user sets up their 2FA, the secret key should never be displayed again.
-
Avoid logging or caching the key anywhere (e.g. logs, temporary files).
-
When storing the
Base32Key
in your database, always encrypt it to protect it at rest. Use a robust encryption library, to encrypt and decrypt the secret key securely. -
Ensure HTTPS Everywhere, if the
otpAuth://
ORL or QR code is ever transmitted over the network (e.g., to a front end), ensure the connection is secured using HTTPS. This prevents interception of the secret key during setup. -
Once the user scans the QR code and sets up 2FA, avoid showing the secret key or QR code again. If the user loses access, you can generate a new key but never reuse the old one.
-
Don’t log the Key or URL: Ensure you don’t log sensitive details like the base32Key or the otpauth:// URL.
-
If an attacker obtains the secret key, they could generate codes indefinitely. To mitigate this:
-
Consider implementing device-based trust where users must re-authenticate for new devices.
-
Use the secret key only for authentication and avoid repurposing it for other use cases.
-
-
Server-Side Key Verification: Instead of exposing the base32Key to the frontend, you can handle the TOTP directly on the server side.
-
Generate and store the secret key on the server.
-
Create and display only the QR code (without sending the raw base32Key to the user).
-
Validate user-entered TOTP codes directly against the server-stored secret key.
-
-
Backup Codes for Recovery
-
Provide users with one-time backup codes during setup to allow them to regain access if they lose their 2FA device. These codes should:
-
Be stored hashed in your database, like passwords.
-
Only be shown to the user during setup.
-
Be invalidated upon use.
-
-
Platform Independence
Since we don’t want to be hard-coupled to an operative system we have to use cross-platform
libraries and classes. QRCoder has a way to work with MS' Bitmap
. But if we were to
put the system in Linux this implementation would break, luckily there’s a QRCoder-ImageSharp
NuGet that can enable us using ImageSharp
to work with graphics in a cross-platform
manner.
Usability
QR Codes
Inside of OtpNet
there are a couple of settings to keep in mind in order to understand
how they serve a purpose.
There’s specifically a line of code that goes like this:
using var qrCodeData = qrGenerator.CreateQrCode(otpAuthUrl, QRCodeGenerator.ECCLevel.Q);
The ECCLevel
Enum hints at different levels for this specific setting, we can
break it down as follows:
-
ECCLevel means Code Error Correction Levels. QR codes specifically use Reed-Solomon error correction, which allows them to recover lost or corrupted data. There are four levels of error correction, each balancing data recovery and storage capacity: — L (Low): Recovers up to 7% of the QR code data. This is the best when the QR Code is expected to be in pristine condition and you need to maximize storage capacity (more data fits in the QR code). — M (Medium): Recovers up to 15% of the QR code data. This is the
default level
and provides a balance between storage capacity and error correction. Good for general use cases where slight damage or distortion might occur. — Q (Quartile): Recovers up to 25% of the QR code data. Used in situations where moderate damage is expected, such as printing QR codes on physical materials. — H (High): Recovers up to 30% of the QR code data. Provides the highest level of error correction but reduces storage capacity (the QR code size will increase to compensate). Suitable for highly critical scenarios such as Outdoor signage, QR codes in rough environments (e.g., packaging or industrial use), artistic designs (logos or overlays) that intentionally obscure parts of the QR code.
A higher ECC levels result in:
-
More redundancy, which increases the QR code’s robustness.
-
Larger QR codes (more pixels or modules) because additional space is used for error correction data.
Lower ECC levels prioritize compactness but are less tolerant to damage.
For general use (like digital screens), the default M
is usually sufficient.
Time Sensitiveness
The TOTP algorithm makes use of the Unix Time, and its epochs, tied into intervals of time that can be then matched with an external secret key, in order to validate if across time, the code provided at a specific time was valid.
The RFC 6238 standard
specifices how OTPs (TOTP) work. It is there that the Verification
Windows is stated to accept slightly out-of-sync codes. (Server looking at a code from
a client, meaning client could be slightly out of sync). The network delay considers
this small of window under the assumptions that there could be: network delays, slight
clock skews between the server and the client device.
By default the verification window accepts one time step before and after the current time step. For example:
-
If the time step interval is 30 seconds: It will accept codes generated up to 30 seconds before and 30 seconds after the current time.
What is a time step?
In TOTP, the time step is a small interval of time (usually 30 seconds) during which a single OTP code is valid. The TOTP algorithm generates a new code for each time step, based on:
-
The shared secret key (
key
in your code). -
The number of time steps that you have elapsed since the Unix epoch
If a code is valid, it means that said code matched to some specific time step for a specific key. You can use that time step number for debugging or logging to see if the user’s device is out-of-sync with the server.
A time step number is based on the intervals that go from the Unix epoch to an arbitrary point in time. Say today 10-December-2024 11:35:22, in 30-second intervals, we would be at step (not actually calculated) 12345. We would use the TOTP algorithm to see if a code provided as an input matches to this step (or at least to the previous and/or following step). If we can match it to them, then the code should be valid.