Skip to main content

Leveling Up Security: Implementing Multi-Factor Authentication (MFA) in Your .NET Core App with step by step

 

Let’s be real—security in web applications is no longer optional, it’s an absolute must. With the rise of cyber threats, traditional username and password combinations are just not enough to keep intruders at bay. That’s where Multi-Factor Authentication (MFA) comes into play.

In this post, we’ll dive into how to implement MFA in your .NET Core app, creating a layered defense that ensures only the right users gain access. We’ll make it simple, practical, and yes, fancy—because why not make security elegant?

Why MFA?

Before we get into the code, let’s answer the big question: Why MFA?

MFA adds an extra step to the authentication process. It's no longer just about "what you know" (like your password), but also about "what you have" (like your phone for a one-time code) or "who you are" (like your fingerprint or face recognition). This significantly reduces the chances of unauthorized access, even if a password is compromised.

Let’s break down a typical MFA scenario:

  1. Step 1: User enters their username and password.
  2. Step 2: User is prompted to enter a second factor, typically a code sent to their phone or email.
  3. Step 3: If both are correct, access is granted.

Sounds like a small extra step, but it goes a long way in fortifying your app.

The Setup: Multi-Factor Authentication in .NET Core

Here’s how we’re going to structure it:

  • Username and Password Authentication (Primary factor)
  • OTP (One-Time Password) via Email/SMS (Secondary factor)

Step 1: The Basics – Setting Up Standard Authentication

Before we add MFA, we need the basics in place—username and password authentication. For this, we’ll use ASP.NET Core Identity, which makes user management and authentication a breeze.

In your Startup.cs, configure authentication and identity services like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication()
        .AddCookie(options => 
        {
            options.LoginPath = "/Account/Login";
            options.LogoutPath = "/Account/Logout";
        });

    services.AddControllersWithViews();
}

Now, your app can handle basic username and password authentication. Time to level up!

Step 2: Adding the Second Factor with OTP

Once the user successfully enters their credentials, we want to prompt them for a second authentication factor—an OTP. Let’s generate and send that OTP via email or SMS.

Generate the OTP

We’ll use the UserManager class from ASP.NET Core Identity to generate the OTP. This OTP will be sent to the user's registered email or mobile number.

public async Task<string> GenerateOtp(ApplicationUser user)
{
    var token = await _userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);
    return token;
}

This method will generate a 6-digit code as the OTP.

Send the OTP

Next, we need to send this OTP to the user. If the user prefers receiving the OTP via email, we can use SMTP as shown earlier. If it’s via mobile, services like Twilio are perfect for sending SMS messages.

For email:

public async Task SendOtpEmailAsync(string email, string otp)
{
    var smtpClient = new SmtpClient("smtp.example.com")
    {
        Port = 587,
        Credentials = new NetworkCredential("your-email@example.com", "password"),
        EnableSsl = true,
    };

    var mailMessage = new MailMessage
    {
        From = new MailAddress("your-email@example.com"),
        Subject = "Your OTP Code",
        Body = $"Your OTP code is {otp}",
        IsBodyHtml = true,
    };

    mailMessage.To.Add(email);
    await smtpClient.SendMailAsync(mailMessage);
}

For SMS (using Twilio):

public async Task SendOtpSmsAsync(string phoneNumber, string otp)
{
    TwilioClient.Init("accountSid", "authToken");

    var message = await MessageResource.CreateAsync(
        body: $"Your OTP code is {otp}",
        from: new Twilio.Types.PhoneNumber("your-twilio-number"),
        to: new Twilio.Types.PhoneNumber(phoneNumber)
    );
}

Step 3: Prompting for the OTP

Once the OTP is sent, the user needs to input it to complete the authentication. Here’s where we prompt the user to enter their OTP.

Create an action method in your AccountController to handle the OTP input.

[HttpGet]
public IActionResult VerifyOtp()
{
    return View();
}

[HttpPost]
public async Task<IActionResult> VerifyOtp(string otp)
{
    var user = await _userManager.GetUserAsync(User);
    var isValid = await _userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, otp);

    if (isValid)
    {
        // OTP is valid, sign the user in
        await _signInManager.SignInAsync(user, isPersistent: false);
        return RedirectToAction("Index", "Home");
    }

    ModelState.AddModelError(string.Empty, "Invalid OTP");
    return View();
}

In the VerifyOtp method, we use ASP.NET Identity’s VerifyTwoFactorTokenAsync method to check if the OTP the user entered is valid.

Step 4: Putting It All Together

Let’s put all the pieces together:

  1. User logs in with username and password.
  2. Upon successful login, the user is prompted to enter their OTP.
  3. The OTP is sent via email or SMS based on their registered preference.
  4. The user enters the OTP, and if it matches, they are granted access.

Here’s a snippet of how the login process looks in your AccountController:

[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        
        if (result.Succeeded)
        {
            var user = await _userManager.FindByEmailAsync(model.Email);
            
            // Generate and send OTP
            var otp = await GenerateOtp(user);
            if (user.PreferredMethod == "Email")
                await SendOtpEmailAsync(user.Email, otp);
            else
                await SendOtpSmsAsync(user.PhoneNumber, otp);
            
            return RedirectToAction("VerifyOtp");
        }
        
        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    }

    return View(model);
}

Why MFA? Because Security Matters!

Adding MFA to your app not only increases security but also builds trust with your users. They can feel confident knowing their data is safe, even if someone gets hold of their password. In this example, you’ve just built a solid, secure authentication system in .NET Core, combining the power of passwords and OTPs for rock-solid security.

Conclusion: It’s Time to Secure Your App!

We’ve walked through implementing Multi-Factor Authentication (MFA) in a .NET Core app using both email and SMS for OTPs. The next step is yours—integrate MFA into your own projects and give your users that extra layer of security. In future posts, we’ll explore even more advanced security features like time-based OTPs, biometrics, and push notifications for multi-factor authentication. Stay tuned!

Now, it's your turn: Have you tried MFA before? What's been your experience? Drop a comment below—let's talk security!

Comments

Popular posts from this blog

Implementing and Integrating RabbitMQ in .NET Core Application: Shopping Cart and Order API

RabbitMQ is a robust message broker that enables communication between services in a decoupled, reliable manner. In this guide, we’ll implement RabbitMQ in a .NET Core application to connect two microservices: Shopping Cart API (Producer) and Order API (Consumer). 1. Prerequisites Install RabbitMQ locally or on a server. Default Management UI: http://localhost:15672 Default Credentials: guest/guest Install the RabbitMQ.Client package for .NET: dotnet add package RabbitMQ.Client 2. Architecture Overview Shopping Cart API (Producer): Sends a message when a user places an order. RabbitMQ : Acts as the broker to hold the message. Order API (Consumer): Receives the message and processes the order. 3. RabbitMQ Producer: Shopping Cart API Step 1: Install RabbitMQ.Client Ensure the RabbitMQ client library is installed: dotnet add package RabbitMQ.Client Step 2: Create the Producer Service Add a RabbitMQProducer class to send messages. RabbitMQProducer.cs : using RabbitMQ.Client; usin...

.NET 10: Your Ultimate Guide to the Coolest New Features (with Real-World Goodies!)

 Hey .NET warriors! 🤓 Are you ready to explore the latest and greatest features that .NET 10 and C# 14 bring to the table? Whether you're a seasoned developer or just starting out, this guide will show you how .NET 10 makes your apps faster, safer, and more productive — with real-world examples to boot! So grab your coffee ☕️ and let’s dive into the awesome . 💪 1️⃣ JIT Compiler Superpowers — Lightning-Fast Apps .NET 10 is all about speed . The Just-In-Time (JIT) compiler has been turbocharged with: Stack Allocation for Small Arrays 🗂️ Think fewer heap allocations, less garbage collection, and blazing-fast performance . Better Code Layout 🔥 Hot code paths are now smarter, meaning faster method calls and fewer CPU cache misses. 💡 Why you care: Your APIs, desktop apps, and services now respond quicker — giving users a snappy experience . 2️⃣ Say Hello to C# 14 — More Power in Your Syntax .NET 10 ships with C# 14 , and it’s packed with developer goodies: Field-Bac...

How Does My .NET Core Application Build Once and Run Everywhere?

One of the most powerful features of .NET Core is its cross-platform nature. Unlike the traditional .NET Framework, which was limited to Windows, .NET Core allows you to build your application once and run it on Windows , Linux , or macOS . This makes it an excellent choice for modern, scalable, and portable applications. In this blog, we’ll explore how .NET Core achieves this, the underlying architecture, and how you can leverage it to make your applications truly cross-platform. Key Features of .NET Core for Cross-Platform Development Platform Independence : .NET Core Runtime is available for multiple platforms (Windows, Linux, macOS). Applications can run seamlessly without platform-specific adjustments. Build Once, Run Anywhere : Compile your code once and deploy it on any OS with minimal effort. Self-Contained Deployment : .NET Core apps can include the runtime in the deployment package, making them independent of the host system's installed runtime. Standardized Libraries ...