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

C# : How can we access private method outside class

Introduction In object-oriented programming, encapsulation is a fundamental principle that restricts direct access to the internal implementation details of a class. Private methods, being part of this internal implementation, are designed to be accessible only within the confines of the class they belong to. However, there might be scenarios where you need to access a private method from outside the class. In this blog post, we'll explore several techniques to achieve this in C#. 1. Reflection: A Powerful Yet Delicate Approach Reflection is a mechanism in C# that allows inspecting and interacting with metadata about types, fields, properties, and methods. While it provides a way to access private methods, it should be used cautiously due to its potential impact on maintainability and performance. using System ; using System . Reflection ; public class MyClass { private void PrivateMethod ( ) { Console . WriteLine ( "This is a private method."

C# : Understanding Types of Classes

In C#, classes serve as the building blocks of object-oriented programming, providing a blueprint for creating objects. Understanding the types of classes and their applications is crucial for designing robust and maintainable software. In this blog, we’ll delve into various types of classes in C#, accompanied by real-world scenarios and code snippets for a practical understanding. 1. Regular (Instance) Classes Definition: Regular classes are the most common type and are used to create instances or objects. They can contain fields, properties, methods, and other members. Example Scenario: A Person class representing individual persons with properties like Name and Age. public class Person { public string Name { get ; set ; } public int Age { get ; set ; } } 2. Static Classes Definition: A static class cannot be instantiated and can only contain static members (methods, properties, fields). It’s often used for utility functions. Example Scenario: A MathUtility cla

C# : 12.0 : Primary constructor

Introduction In C# 12.0, the introduction of the "Primary Constructor" simplifies the constructor declaration process. Before delving into this concept, let's revisit constructors. A constructor is a special method in a class with the same name as the class itself. It's possible to have multiple constructors through a technique called constructor overloading.  By default, if no constructors are explicitly defined, the C# compiler generates a default constructor for each class. Now, in C# 12.0, the term "Primary Constructor" refers to a more streamlined way of declaring constructors. This feature enhances the clarity and conciseness of constructor declarations in C# code. Lets see an simple example code, which will be known to everyone. public class Version { private int _value ; private string _name ; public Version ( int value , string name ) { _name = name ; _value = value ; } public string Ve