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:
- Step 1: User enters their username and password.
- Step 2: User is prompted to enter a second factor, typically a code sent to their phone or email.
- 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:
- User logs in with username and password.
- Upon successful login, the user is prompted to enter their OTP.
- The OTP is sent via email or SMS based on their registered preference.
- 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
Post a Comment