.NET Core  

Implementing TOTP (Time-Based One-Time Password) MFA in .NET Core

Introduction

TOTP (Time-based One-Time Password) is a secure two-factor authentication (2FA) method that generates temporary, single-use codes for user verification. These codes refresh at fixed intervals—usually every 30 seconds—and are synchronized with the current time. By dynamically changing the valid code, TOTP enhances security compared to static passwords, significantly reducing the risk of unauthorized access even if a code is intercepted, as it expires quickly.

This method ensures that each authentication attempt requires a fresh, time-sensitive code, making it a robust defense against replay attacks and credential theft. The process works by combining a shared secret key (known only to the server and the user) with the current time to generate a one-time password. This password is then used to verify the user's identity in addition to their regular login credentials. TOTP is widely used in various applications, such as securing online accounts, mobile banking, and more, due to its effectiveness in enhancing security without being overly complicated for users.

Why MFA?

Multi-Factor Authentication (MFA) significantly enhances application security by requiring users to verify their identity using multiple methods. While traditional authentication uses only a username and password, MFA adds a second layer — typically a time-based one-time password (TOTP), which expires every 30 seconds.

This article walks you through implementing TOTP-based MFA in a real-world ASP.NET Core setup consisting of,

  • A Web API project to handle backend authentication logic.
  • A MVC UI project to handle frontend interaction with the user.

Project Architecture

Our application consists of two projects.

totp_dotnet_api– ASP.NET Core Web API

  • Contains TOTP Controller having login, setup and verify three endpoints.
  • One TotpService class for logic handling

totp_dotnet_mvc– ASP.NET Core MVC

Contains login and setup TOTP pages with controller.

Folder Structure

Folder Structure

Now let's create endpoints in the Web API project. The Web API project provides three key endpoints to manage user login and TOTP-based multi-factor authentication. Each of these plays a specific role in the authentication flow: login, MFA setup, and verification.

Web API project

 [HttpPost("login")]
 public IActionResult Login([FromBody] LoginRequest request)
 {
     if (request == null)
         return BadRequest("Invalid request");

     var response = _totpService.Login(request.Username, request.Password);
     return Ok(response);
 }

 [HttpPost("setup")]
 public IActionResult SetupTotp([FromBody] TotpSetupRequest request)
 {
     if (request == null)
         return BadRequest("Invalid request");

     var response = _totpService.SetupTotp(request.Username);
     return Ok(response);
 }

 [HttpPost("verify")]
 public IActionResult VerifyTotp([FromBody] TotpVerificationRequest request)
 {
     if (request == null)
         return BadRequest("Invalid request");

     var response = _totpService.VerifyTotp(request.Username, request.Code);
     return Ok(response);
 }

In the code given above, we created endpoints for login, setup, and verification. Now lets create one TotpService.cs class inside Services folder as given in the folder structure. This class will contain different methods calling from controller. This service is responsible for all the logic behind.

  • User login validation
  • Setting up TOTP
  • Verifying TOTP codes

In this example, there’s no real database — you're using static dictionaries (_userPasswords and _userSecrets) to simulate user data and their MFA secrets.

public class TotpService : ITotpService {
  private readonly Dictionary<string, string> _userSecrets =
      new Dictionary<string, string> {
        { "alice", "JBSWY3DPEHPK3PXP" },  // Example Base32 secret
        { "bob", "KRSXG5DSNBSWY3DP" },
        { "charlie", "MZXW6YTBOI======" }
      };

  private readonly Dictionary<string, string> _userPasswords =
      new Dictionary<string, string> {
        { "alice", "password123" }, { "bob", "123456" }, { "charlie", "qwerty" }
      };

  public LoginResponse Login(string username, string password) {
    // var key = KeyGeneration.GenerateRandomKey(20);
    // var base32Secret = Base32Encoding.ToString(key);
    _userSecrets.TryGetValue(username, out var base32Secret);

    // In a real application, you would verify against a database
    if (!_userPasswords.ContainsKey(username) ||
        _userPasswords[username] != password) {
      return new LoginResponse { Success = false,
                                 Message = "Invalid credentials",
                                 IsTotpEnabled = false, Secret = null };
    }
    // If TOTP is not enabled, show setup page
    var setupResponse = SetupTotp(username);
    var response = new LoginResponse {
      Success = true,        Message = "Login successful",
      IsTotpEnabled = false, QrCodeUrl = setupResponse.QrCodeUrl,
      Secret = base32Secret,
    };
    return response;
  }

  public LoginResponse SetupTotp(string username) {
    _userSecrets.TryGetValue(username, out var base32Secret);
    // Generate a random secret key
    // var key = KeyGeneration.GenerateRandomKey(20);
    // var base32Secret = Base32Encoding.ToString(key);
    // Store the secret for the user
    _userSecrets[username] = base32Secret;

    return new LoginResponse {
      Success = true, Message = "TOTP setup successful", IsTotpEnabled = true,
      QrCodeUrl = GenerateQrCode(username, base32Secret), Secret = base32Secret
    };
  }

  public TotpVerifyResponse VerifyTotp(string username, string code) {
    if (!_userSecrets.TryGetValue(username, out var secret)) {
      return new TotpVerifyResponse {
        Success = false, Message = "TOTP not set up for this user"
      };
    }

    var totp = new Totp(Base32Encoding.ToBytes(secret));
    var isValid = totp.VerifyTotp(code, out _, new VerificationWindow(1, 1));

    return new TotpVerifyResponse {
      Success = isValid,
      Message = isValid ? "TOTP verification successful" : "Invalid TOTP code"
    };
  }

  private static string GenerateQrCode(string username, string secret) {
    var issuer = "TotpPOC";
    var qrCodeUrl =
        $"otpauth://totp/{issuer}:{username}?secret={secret}&issuer={issuer}";

    using var qrGenerator = new QRCodeGenerator();
    var qrCodeData =
        qrGenerator.CreateQrCode(qrCodeUrl, QRCodeGenerator.ECCLevel.Q);
    using var qrCode = new PngByteQRCode(qrCodeData);
    var qrCodeBytes = qrCode.GetGraphic(20);
    using var mStream = new MemoryStream(qrCodeBytes);
    var base64Image = Convert.ToBase64String(mStream.ToArray());

    return $"data:image/png;base64,{base64Image}";
  }
}
public interface ITotpService {
  LoginResponse Login(string username, string password);
  LoginResponse SetupTotp(string username);
  TotpVerifyResponse VerifyTotp(string username, string code);
}

Method: Login

  • Tries to fetch the user's TOTP secret.
  • Validates username and password from _userPasswords.
  • If valid, assumes TOTP is not yet enabled and:
    • Calls SetupTotp() to provide the TOTP QR code and secret.
    • Returns a LoginResponse indicating success and includes the QR code URL + secret.

Note. In a production app, you'd only setup TOTP during registration or profile settings, not on every login.

Method: SetupTotp

  • Retrieves the user’s existing TOTP secret from _userSecrets.
  • Prepares a LoginResponse:
    • IsTotpEnabled = true
    • Generates a TOTP-compatible QR Code URI
    • Converts the QR code into a Base64 string so it can be embedded directly in an <img> tag in your HTML page.

In real scenarios, you'd generate a new key here and persist it securely. You’re currently using a predefined one.

Method: VerifyTotp

  • Fetches the Base32 secret for the user.
  • Uses the Otp.NET library to:
    • Decode the Base32 secret.
    • Create a Totp instance.
    • Verify the user-supplied code against the current TOTP window (±1 interval).
  • Returns a success/failure response.

MVC UI Integration for TOTP MFA

Now that we’ve built the backend logic and API endpoints for user login and TOTP verification, it’s time to integrate a simple ASP.NET Core MVC frontend. The UI will guide the user through the login and 2FA setup process. The home page serves as the entry point. It provides two options.

  • Login – Triggers the standard login flow.
  • Enable 2FA – Directly initiates the TOTP setup process (for users who want to secure their account with MFA proactively).

 TOTP setup

When the user clicks the Login button.

  • They are redirected to the login form where they enter: Username, Password
  • On submission, the Login API is called via HttpClient.
  • The API response decides the next step:
    • If 2FA is enabled, the user is redirected to the TOTP Verification screen.
    • If 2FA is not enabled, the app initiates the TOTP Setup screen so the user can configure MFA.

Note: As of now in this demo you will see both the screen since we are not using database to store 'IsEnabled' key for the users.

LoginPage

Enable 2FA (SetupTotp.cshtml)

  • When a user clicks Enable 2FA on the homepage.
  • The Setup API is triggered using the logged-in user’s username.
  • The QR code is returned from the API as a Base64 image.
  • The user is shown the QR code on the screen and asked to scan it using an authenticator app.
  • A text field allows them to enter the 6-digit TOTP from their app.

Setup

TOTP Verification (VerifyTotp.cshtml)

If the user logged in and already has MFA enabled, they are immediately redirected here to complete the second step of authentication.

  • The user enters the 6-digit code generated by their authenticator app.
  • The Verify API is called.
  • If verification is successful:
    • The user is authenticated and redirected to the protected area.
    • The JWT token is stored in session or cookies.
  • If it fails: Show an error message and allow retry.

Security Considerations

  1. Secret Storage
    • Always encrypt TOTP secrets in database
    • Never log or display the secret after initial setup
  2. Rate Limiting
    • Implement rate limiting on TOTP verification attempts
    • Exponential delays after failed attempts
  3. Time Synchronization
    • Ensure server time is synchronized with NTP
    • Allow small window for clock drift (±1-2 time steps)
  4. Session Management
    • Don't persist 2FA state in cookies
    • Use server-side session storage

Conclusion

Implementing TOTP-based MFA in .NET Core provides a robust security layer for your applications. This implementation supports both web and API authentication flows, giving users flexibility while maintaining security. The solution can be extended with additional features like SMS fallback or hardware token support.

We covered,

  • Backend Setup: A clean Web API using .NET Core that handles login, TOTP setup, and verification.
  • Security Logic: How to generate and verify TOTP codes using static secrets and the Otp.NET library.
  • Frontend UI (MVC): A user-friendly interface that enables login, QR code showing, and TOTP verification using views and form-based workflows.

Thank You, and Stay Tuned for More!

More Articles from my Account