Implementing Two-Factor Authentication with Google Authenticator in ASP.NET Core

By FoxLearn 3/19/2025 8:18:53 AM   275
In this article, we'll explore how to implement two-factor authentication (2FA) in an ASP.NET Core web application using Time-Based One-Time Password (TOTP) apps, like Google Authenticator and Authy.

The process is simple: the user scans a QR code, sets up their TOTP app, and then uses it to generate time-based passwords for authentication.

Why Choose TOTP Over SMS or Email for 2FA?

There are multiple methods to implement multi-factor authentication (MFA), with SMS being one of the common ones. The SMS approach typically sends a code to the user’s phone or email, which they enter to authenticate. While this adds security over single-factor authentication, it’s no longer recommended due to the numerous vulnerabilities associated with SMS.

For instance, SMS-based MFA is susceptible to attacks such as SIM swapping, interception, and phishing. For better security, it’s advisable to opt for TOTP, which is far more resilient against such threats.

There’s also a newer alternative called Passkeys, which removes the need for MFA altogether, making passwords obsolete.

How TOTP Works

TOTP is straightforward. The backend generates a secret key when the user requests it and returns it to the frontend. The frontend then creates a QR code for the user to scan with their TOTP app (e.g., Google Authenticator). Once scanned, the app stores the secret and generates real-time digits tied to the current timestamp. Meanwhile, the app and the server use the same algorithm to generate one-time passwords that change every 30 seconds.

Implementing TOTP in Your ASP.NET Core Application

Next, We will learn how to add 2fa to an asp.net app

1. Install Required Packages

We’ll need two packages to implement TOTP:

  • Otp.NET – This will handle MFA secret generation and TOTP calculation. Install it via NuGet: Install-Package Otp.NET -Version 1.3.0
  • QRCodeJS – This JavaScript library generates QR codes. Download it and place it in your project’s wwwroot/lib/qrcode/qrcode.js directory.

2. Create the User Model

We have a simple User model where we store the username, password, and MFA secret.

public class Users
{
    [Key]
    public int Id { get; set; }
    
    [Required]
    [MaxLength(50)]
    public string Username { get; set; }

    [Required]
    public string Password { get; set; }

    public string? MFASecret { get; set; }
}

3. Account Controller

In the AccountController, we will manage the logic for enabling and verifying 2FA. The controller uses the DbContext to handle CRUD operations for users and stores the encryption key for secret storage.

public class AccountController : Controller
{
    private readonly AppDbContext _dbContext;
    private readonly byte[] key;

    public AccountController(AppDbContext dbContext)
    { 
        _dbContext = dbContext;
        key = Encoding.UTF8.GetBytes("your-randomly-generated-key");
    }
}

Note: Ensure you replace "your-randomly-generated-key" with a secure key. You can generate a 32-byte key using a random password generator.

4. Registering MFA Secret

In the view page for MFA registration, we check whether the user already has an MFA secret. If they don’t, we generate one and send it to the frontend to display a QR code. The QR code is created using the QRCodeJS library.

@{
    ViewData["Title"] = "User Management";
    string username = @Context.Session.GetString("Username");
}

@model string?

<h1>@ViewData["Title"]</h1>

@if (!string.IsNullOrEmpty(ViewBag.MFAError))
{
    if (ViewBag.MFAError == "WrongCode")
    {
        <div class="alert alert-info w-100" role="alert">
            Wrong Code! Please try again!
        </div>
    }
}

<div class="row g-4">
    @if (!string.IsNullOrEmpty(Model))
    {
        <div class="col-xl-3 col-lg-6 col-md-12">
            <h4 class="mb-1 pt-2 text-center">Activate two-factor authentication</h4>
            <p class="text-center">Scan the QR Code to your OTP App</p>
            <script src="~/lib/qrcode/qrcode.js"></script>
            <div id="qrcode-container" class="text-center ms-3">
                <div id="qrcode"></div>
            </div>
            <script type="text/javascript">
                new QRCode(document.getElementById("qrcode"),
                    'otpauth://totp/@username?secret=@Model&issuer=myapp.com');
            </script>
            <br />
            <p class="text-center">or enter the code manually</p>
            <p class="text-center text-muted">@Model</p>
            <form id="twoStepsForm" action="/Account/RegisterMFASecret" method="POST">
                <div class="mb-3">
                    <input type="text" class="form-control" maxlength="6" name="AuthCode"/>
                </div>
                <button class="btn btn-primary w-100">Send</button>
            </form>
        </div>
    }
    else
    {
        <h4 class="mb-1 pt-2 text-center">You already have 2FA enabled! Great!</h4>
    }
</div>

5. Verifying the MFA Code

After the user enters their MFA code, we check it against the one generated on the server. If they match, the user is authenticated.

[HttpPost]
public IActionResult VerifyMFA(string AuthCode)
{
    var UserId = HttpContext.Session.GetInt32("OTPUserId");
    var Counter = HttpContext.Session.GetInt32("OTPCounter");

    if (UserId != null && Counter.HasValue && Counter > 0)
    {
        var user = _dbContext.Users.Where(s => s.Id == UserId).FirstOrDefault();

        if (user == null || user.MFASecret == null)
        {
            Response.StatusCode = 400;
            return Redirect("Login");
        }

        string secret = Decrypt(user.MFASecret);
        var totp = new Totp(Base32Encoding.ToBytes(secret));
        var totpCode = totp.ComputeTotp(DateTime.UtcNow.AddSeconds(-1));

        if (AuthCode == totpCode)
        {
            HttpContext.Session.Remove("OTPUserId");
            HttpContext.Session.Remove("OTPCounter");
            HttpContext.Session.SetString("Username", user.Username);
            HttpContext.Session.SetInt32("UserId", user.Id);
            return Redirect("~/Home/Index");
        }
        else
        {
            int remainingCounter = Counter.Value - 1;

            if (remainingCounter <= 0)
            {
                HttpContext.Session.Remove("OTPUserId");
                HttpContext.Session.Remove("OTPCounter");
                ViewBag.CredentialError = "You need to login again!";
                return Redirect("Login");
            }

            ViewBag.MFAError = "Wrong code! Try again!";
            HttpContext.Session.SetInt32("OTPCounter", remainingCounter);
            return View("MFAVerification");
        }
    }
    else
    {
        Response.StatusCode = 401;
        return Redirect("Login");
    }
}

Storing and Encrypting the Secret

To prevent exposure of sensitive data, we store the TOTP secret in an encrypted format. Here’s how you can encrypt and decrypt the secret using AES:

private string Encrypt(string input)
{
    using (Aes aes = Aes.Create())
    {
        byte[] iv = new byte[16];
        new Random().NextBytes(iv);
        aes.Key = key;
        aes.IV = iv;

        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

        using (MemoryStream ms = new MemoryStream())
        {
            using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
            {
                using (StreamWriter sw = new StreamWriter(cs))
                {
                    sw.Write(input);
                }
            }

            return "enc:" + Convert.ToBase64String(iv) + ":::" + Convert.ToBase64String(ms.ToArray());
        }
    }
}

private string Decrypt(string input)
{
    if (string.IsNullOrEmpty(input) || !input.StartsWith("enc:"))
        throw new Exception("The input isn't encrypted");

    using (Aes aes = Aes.Create())
    {
        input = input.Substring(4);
        var parts = input.Split(":::");

        aes.Key = key;
        aes.IV = Convert.FromBase64String(parts[0]);

        ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

        using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(parts[1])))
        {
            using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
            {
                using (StreamReader sr = new StreamReader(cs))
                {
                    return sr.ReadToEnd();
                }
            }
        }
    }
}

Implementing TOTP-based two-factor authentication in your ASP.NET Core application significantly enhances security. By following the steps above, you can ensure that your users are better protected from various types of attacks while still offering a seamless authentication experience.