Customize 2FA in .NET Core MVC App Without Identity (Part -2)

Picture of Vivasoft Team
Vivasoft Team
Published on
20.10.2025
Time to Read
8 min

[rank_math_breadcrumb]

2FA in .NET Core MVC App Without Identity
Table of Contents

In this blog we will implement 2FA in a Web application where Username/Password authentication is implemented, which also doesn’t use ASP.NET Identity, but it does rely on ASP.NET’s core authentication semantics to handle Cookie authentication. I’ve written about how to implement Custom Username Password Authentication in Part One.

Process of Implementing Custom Username Password Authentication

Step 1: Install the NuGet packages:

1.Otp.NET

Step 2: Update Entities

We’ve introduced two new properties to the User class:

  • IsTwoFactorEnabled: This boolean property indicates whether two-factor authentication (2FA) is enabled for the user.
  • SecretKey: This nullable string property stores the secret key associated with the user’s two-factor authentication setup. The secret key is used in conjunction with authentication apps (such as Microsoft Authenticator) to generate one-time codes for verification.

User.cs

				
					using System.ComponentModel.DataAnnotations;
 
namespace TwoFactorAuth.Entities
{
	public class User
	{
    	[Key]
    	public int Id { get; set; }
    	public string Name { get; set; }
    	public string Username { get; set; }
    	public string Password { get; set; }
    	public bool IsTwoFactorEnabled { get; set; } // New property for two-factor authentication
	public string? SecretKey { get; set; } // New property to store secret key for two-factor authentication	}
}

				
			

Step 3: Update Database using Migration:

Migrations are used in Entity Framework Core to apply changes to the database schema as the data model changes. This can be done using the following command in the Package Manager Console (Tools > NuGet Package Manager > Package Manager Console):

– Add-Migration user_update
– Update-Database

After done update database command will execute successfully, just check the database’s table

database’s table

Step 4: Create ViewModel EnableAuthenticatorViewModel in Models folder:

				
					namespace TwoFactorAuth.Models
{
	public class EnableAuthenticatorViewModel
	{
    	public string Username { get; set; }
    	public string SecretKey { get; set; }
    	public string AuthenticatorUri { get; set; }
    	public string Code { get; set; }
	}
}

				
			

Step 5: Add Methods to UserService:

In UserService, add three new methods: GetUser, GenerateTwoFactorInfo, EnableAuthenticator and DisableTwoFactorAuth.

GetUser: Retrieves a user based on a given expression. Fetching user data required for various operations.

GenerateTwoFactorInfo: Generates a secret key and a corresponding QR code URL for setting up 2FA.

EnableAuthenticator: To enable 2FA for a user. Which verify the provided authentication code and update the user’s SecretKey and set IsTwoFactorEnabled to true.

DisableTwoFactorAuth: To disable 2FA for a user. Which reset the user’s SecretKey and Update IsTwoFactorEnabled to false.

Updated UserService will be as follows:

				
					using Microsoft.EntityFrameworkCore;
using OtpNet;
using System.Linq.Expressions;
using System.Text;
using TwoFactorAuth.Entities;
using TwoFactorAuth.Models;
 
namespace TwoFactorAuth.Services
{
	public interface IUserService
	{
    	Task<bool> RegisterUser(RegisterViewModel registerRequest);
    	Task<bool> AuthenticateUser(LoginViewModel loginRequest);
    	Task<User> GetUser(Expression<Func<User, bool>> expression);
    	Task<(string secretKey, string qrCodeUrl)> GenerateTwoFactorInfo(string username);
    	Task<bool> EnableAuthenticator(EnableAuthenticatorViewModel model);
    	Task<bool> DisableAuthenticator(string username);
	}
	public class UserService : IUserService
	{
    	private AppDbContext _dbContext;
    	public UserService(AppDbContext appDBContext)
    	{
        	_dbContext = appDBContext;
    	}
 
    	public async Task<bool> RegisterUser(RegisterViewModel registerRequest)
    	{
        	try
        	{
            	bool isExist = await _dbContext.Users.AnyAsync(u => u.Username == registerRequest.Username);
            	if (isExist)
            	{
                	return false;
            	}
            	User user = new User();
 
            	user.Name = registerRequest.Name;
            	user.Username = registerRequest.Username;
 
            	user.Password = BCrypt.Net.BCrypt.HashPassword(registerRequest.Password);
 
            	var res = await _dbContext.Users.AddAsync(user);
 
            	await _dbContext.SaveChangesAsync();
 
            	return true;
        	}
        	catch (Exception ex)
        	{
            	return false;
        	}
 
    	}
    	public async Task<bool> AuthenticateUser(LoginViewModel loginRequest)
    	{
        	try
        	{
            	var user = await _dbContext.Users.FirstOrDefaultAsync(m => m.Username == loginRequest.Username);
 
            	if (user != null && BCrypt.Net.BCrypt.Verify(loginRequest.Password, user.Password))
            	{
                	return true;
            	}
            	else
            	{
                	return false;
            	}
 
        	}
        	catch (Exception ex)
        	{
            	return false;
        	}
 
    	}
    	public async Task<User> GetUser(Expression<Func<User, bool>> expression)
    	{
        	User user = await _dbContext.Users.FirstOrDefaultAsync(expression);
        	return user;
    	}
    	public async Task<(string secretKey, string qrCodeUrl)> GenerateTwoFactorInfo(string username)
    	{
        	const string validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
        	var random = new Random();
 
        	// Generate a random secret key
        	var secretKeyBuilder = new StringBuilder(16);
        	for (int i = 0; i < 16; i++)
        	{
                secretKeyBuilder.Append(validChars[random.Next(validChars.Length)]);
        	}
 
        	var secretKey = secretKeyBuilder.ToString();
 
        	var encodedUsername = Uri.EscapeDataString(username);
        	var qrCodeUrl = $"otpauth://totp/{encodedUsername}?secret={secretKey}&issuer=TwoFactorAuthApp";
 
        	return (secretKey, qrCodeUrl);
    	}
 
    	public async Task<bool> EnableAuthenticator(EnableAuthenticatorViewModel model)
    	{
        	try
        	{
            	var totp = new Totp(Base32Encoding.ToBytes(model.SecretKey));
            	var verified = totp.VerifyTotp(model.Code, out _, new VerificationWindow(2, 2)); // Adjust window size as needed
            	if (verified)
            	{
                	var userDb = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == model.Username);
                	if (userDb != null)
                	{
                    	userDb.SecretKey = model.SecretKey;
                        userDb.IsTwoFactorEnabled = true;
                    	var res = _dbContext.Users.Update(userDb);
                    	await _dbContext.SaveChangesAsync();
                    	return true;
                	}
            	}
            	return false;
        	}
        	catch (Exception ex)
        	{
 
            	return false;
        	}
 
    	}
 
    	public async Task<bool> DisableAuthenticator(string username)
    	{
        	try
        	{
            	var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username);
            	if (user != null)
            	{
                	user.SecretKey = string.Empty;
                	user.IsTwoFactorEnabled = false;
                	var res = _dbContext.Users.Update(user);
                	await _dbContext.SaveChangesAsync();
                	return true;
            	}
            	return false;
        	}
        	catch (Exception ex)
        	{
            	return false;
        	}
 
    	}
	}
}

				
			

Step 6: HomeController changes:

Modify the Index action to retrieve the user based on the current user’s identity and pass it to the view.

				
					using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using TwoFactorAuth.Entities;
using TwoFactorAuth.Models;
using TwoFactorAuth.Services;
 
namespace TwoFactorAuth.Controllers
{
	[Authorize]
	public class HomeController : Controller
	{
    	private readonly ILogger<HomeController> _logger;
    	private readonly IUserService _userService;
 
    	public HomeController(ILogger<HomeController> logger,IUserService userService)
    	{
        	_logger = logger;
        	_userService = userService;
    	}
 
    	public async Task<IActionResult> Index()
    	{
        	User user = await _userService.GetUser(m => m.Username == User.Identity.Name);
        	return View(user);
    	}
 
    	public IActionResult Privacy()
    	{
        	return View();
    	}
 
    	[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    	public IActionResult Error()
    	{
        	return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    	}
	}
}

				
			

Step 7: Index.cshtml Changes:

Change the model declaration to TwoFactorAuth.Entities.User, indicating that the view expects a User object.
Display the user’s name and provide a link to either enable or disable two-factor authentication based on whether it’s already enabled or not.

				
					@model TwoFactorAuth.Entities.User
 
@{
	ViewData["Title"] = "Home Page";
}
 
<div class="text-center">
	<h1 class="display-4">Welcome</h1>
	<h2>@Model.Name</h2>
	<h4>
    	@if (@Model.IsTwoFactorEnabled)
    	{
 
        	<a class="text-" asp-area="" asp-controller="Account" asp-action="DisableAuthenticator">Disable Two Factor Authentication</a>
    	}
    	else
    	{
        	<a class="text-success" asp-area="" asp-controller="Account" asp-action="EnableAuthenticator">Enable Two Factor Authentication</a>
    	}
	</h4>
</div>

				
			

Now if you run the application home page will look like this:

application home page

Step 8: Implement two factor authentication enabling or disabling functionality:

Add two new action on AccountController as follows:

EnableAuthenticator [HttpGet, Authorize]: Responsible for rendering the view where users can enable two-factor authentication (2FA) for their account. Only authenticated users are allowed to access this action.

Retrieves the current user’s username from the identity context. Calls the GenerateTwoFactorInfo method from the UserService to generate a secret key and a corresponding QR code URL for setting up 2FA.

Populates the EnableAuthenticatorViewModel with the generated secret key and QR code URL.

				
					[HttpGet,Authorize]
public async Task<IActionResult> EnableAuthenticator()
{
	EnableAuthenticatorViewModel model = new EnableAuthenticatorViewModel();
 
	model.Username = User.Identity.Name;
 
	(string secretKey, string qrCodeUrl) =await _userService.GenerateTwoFactorInfo(model.Username);
 
	model.SecretKey = secretKey;
 
	model.AuthenticatorUri = qrCodeUrl;
 
	return View(model);
}

				
			

EnableAuthenticator [HttpPost, Authorize]: Handles the form submission when users attempt to enable 2FA for their account. Similar to the previous action, only authenticated users are authorized to access this action. Accepts the EnableAuthenticatorViewModel containing the verification code entered by the user.

Calls the EnableAuthenticator method from the UserService to validate the verification code and enable 2FA for the user if the code is valid. If the verification is successful, redirects the user to the home page.

If the verification fails, adds a model error to the ModelState indicating an invalid verification code and returns the view with the original model, allowing the user to retry the process.

				
					[HttpPost, Authorize]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel request)
{
	var res = await _userService.EnableAuthenticator(request);
 
	if (res)
	{
    	return RedirectToAction("Index","Home");
	}
 
	ModelState.AddModelError("", "Invalid verification code");
	return View(request);
}

				
			

DisableAuthenticator [HttpGet, Authorize]: Facilitates the process of disabling two-factor authentication (2FA) for a user’s account. Only authenticated users are authorized to access this action. Calls the DisableAuthenticator method from the UserService to disable 2FA for the user’s account. Upon successful completion, redirects the user to the home page.

				
					[HttpGet, Authorize]
public async Task<IActionResult> DisableAuthenticator()
{
    string username = User.Identity.Name;
    await _userService.DisableTwoFactorAuth(username);
    return RedirectToAction("Index", "Home");
}

				
			

Add view named EnableAuthenticator in Account folder under Views folder: Serves as the user interface for enabling two-factor authentication. The view dynamically generates a QR code based on the AuthenticatorUri, facilitating easy scanning by the authenticator app and a form allows users to input the verification code generated by the authenticator app. Upon submission of the form, the entered verification code is sent to the server for validation.

Download qrcode.js from here https://davidshimjs.github.io/qrcodejs/ and place under wwwroot\lib\qrcode .

Use the library to generate the QR code on the EnableAuthenticator.cshtml page

				
					@model EnableAuthenticatorViewModel
@{
	ViewData["Title"] = "Enable authenticator";
}
 
<h4>@ViewData["Title"]</h4>
<div>
	<p>To use an authenticator app go through the following steps:</p>
	<ol class="list">
    	<li>
        	<p>
            	Download a two-factor authenticator app like Microsoft Authenticator for
            	<a href="https://go.microsoft.com/fwlink/?Linkid=825071">Windows Phone</a>,
            	<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
            	<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a>
        	</p>
    	</li>
    	<li>
        	<p>Scan the QR Code or enter this key <kbd>@Model.SecretKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
        	@* <div class="alert alert-info">To enable QR code generation please read our <a href="https://go.microsoft.com/fwlink/?Linkid=852423">documentation</a>.</div> *@
        	<div id="qrCode"></div>
        	<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
    	</li>
    	<li>
        	<p>
            	Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
            	with a unique code. Enter the code in the confirmation box below.
        	</p>
        	<div class="row">
            	<div class="col-md-6">
                	<form method="post" asp-action="EnableTwoFactorAuth">
                    	<div class="form-group">
                        	<label asp-for="Code" class="control-label">Verification Code</label>
                        	<input asp-for="Code" class="form-control" autocomplete="off" />
                        	<input asp-for="SecretKey" hidden />
                        	<input asp-for="AuthenticatorUri" hidden />
                        	<input asp-for="Username" hidden />
                        	<span asp-validation-for="Code" class="text-danger"></span>
                    	</div>
                    	<button type="submit" class="btn btn-outline-primary m-2">Verify</button>
                    	<div asp-validation-summary="ModelOnly" class="text-danger"></div>
                	</form>
            	</div>
        	</div>
    	</li>
	</ol>
</div>
@section Scripts {
	@await Html.PartialAsync("_ValidationScriptsPartial")
	<script src="~/lib/qrcodejs/qrcode.js"></script>
	<script type="text/javascript">
 
    	new QRCode(document.getElementById("qrCode"),
        	{
            	text: "@Html.Raw(Model.AuthenticatorUri)",
            	width: 200,
            	height: 200
        	});
	</script>
}

				
			

Now when click on Enable Two Factor Authentication of home page, we will navigate here. Scan the QR code, enter verification code, click verify will enable 2FA.

enter verification code

Step 9: Implement second step of login process:

Add ViewModel TwoStepLoginViewModel In Models folder:

				
					namespace TwoFactorAuth.Models
{
	public class TwoStepLoginViewModel
	{
    	public string Username { get; set; }
    	public string Code { get; set; }
	}
}

				
			

*Add Method to UserService

In UserService add a new method VerifyTwoFactorAuthentication is responsible for validating the two-factor authentication (2FA) code entered by the user during the login process.

				
					public async Task<bool> VerifyTwoFactorAuthentication(TwoStepLoginViewModel model)
{
	try
	{
    	var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == model.Username);
 
    	if (user != null && !string.IsNullOrEmpty(user.SecretKey) && !string.IsNullOrEmpty(model.Code))
    	{
        	var totp = new Totp(Base32Encoding.ToBytes(user.SecretKey));
        	var verifed = totp.VerifyTotp(model.Code, out _, new VerificationWindow(2, 2)); // Adjust window size as needed
        	return verifed;
    	}
    	return false;
	}
	catch (Exception ex)
	{
 
    	return false;
	}
 
}

				
			

Add New action LoginTwoStep in ActionController:

LoginTwoStep [HttpPost] : Handles the submission of the two-step login form, where users input their verification code for two-factor authentication (2FA). Receives the TwoStepLoginViewModel containing the username and verification code entered by the user.

Calls the VerifyTwoFactorAuthentication method from the UserService to validate the verification code against the user’s stored secret key. If the verification is successful Signs in the user and Redirects the user to the home page. If the verification fails, add a model error to the ModelState indicating an invalid verification code.

				
					[HttpPost]
public async Task<IActionResult> LoginTwoStep(TwoStepLoginViewModel request)
{
	bool isVerified = await _userService.VerifyTwoFactorAuthentication(request);
	if (isVerified)
	{
    	var claims = new List<Claim>
        	{
            	new Claim(ClaimTypes.Name, request.Username)
        	};
    	ClaimsIdentity userIdentity = new ClaimsIdentity(claims, "login");
    	ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
 
    	await HttpContext.SignInAsync(principal);
    	return RedirectToAction("Index", "Home");
	}
 
	ModelState.AddModelError("", "Invalid verification code");
	return View(request);
}

				
			

Add a view named LoginTwoStep in the Account folder under Views folder: Serves as the user interface for validating the two-factor authentication (2FA) code during the login process. It allows users to input the verification code generated by their authenticator app and submit it for validation.

				
					@model TwoFactorAuth.Models.TwoStepLoginViewModel
@{
	ViewData["Title"] = "Two Factor validation";
}
 
<h4>@ViewData["Title"]</h4>
<div>
	<ul class="list">
    	<li>
        	<p>
            	Please enter the two-factor validation code from your Authenticator app into the textbox below and click Validate button.
        	</p>
        	<div class="row">
            	<div class="col-md-6">
                	<form method="post" asp-action="LoginTwoStep">
                    	<div class="form-group">
                        	<label asp-for="Code" class="control-label">Validation Code</label>
                        	<input asp-for="Code" class="form-control" autocomplete="off" />
                        	<input asp-for="Username" hidden />
                        	<span asp-validation-for="Code" class="text-danger"></span>
                    	</div>
                    	<button type="submit" class="btn btn-outline-primary m-2">Validate</button>
                    	<div asp-validation-summary="ModelOnly" class="text-danger"></div>
                	</form>
            	</div>
        	</div>
    	</li>
	</ul>
</div>
@section Scripts {
	@await Html.PartialAsync("_ValidationScriptsPartial")
}

				
			

Modify Login action of AccountController:

Enhances the login process to accommodate two-factor authentication (2FA) for users who have it enabled. If authentication is successful, Retrieves the user’s information from the database,

Checks if two-factor authentication (IsTwoFactorEnabled) is enabled for the user. If 2FA is enabled, Redirects the user to the “LoginTwoStep” view, passing a TwoStepLoginViewModel containing the username. If 2FA is not enabled Proceeds with the same logic as the old login action.

				
					[HttpPost]
public async Task<IActionResult> Login(LoginViewModel request)
{
	if (ModelState.IsValid)
	{
    	bool isAuthenticated = await _userService.AuthenticateUser(request);
 
    	if (isAuthenticated)
    	{
        	var user = await _userService.GetUser(m => m.Username == request.Username);
        	if (user.IsTwoFactorEnabled)
        	{
            	return View("LoginTwoStep", new TwoStepLoginViewModel { Username = user.Username });
        	}
        	var claims = new List<Claim>
        	{
            	new Claim(ClaimTypes.Name, request.Username)
        	};
        	ClaimsIdentity userIdentity = new ClaimsIdentity(claims, "login");
        	ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
 
        	await HttpContext.SignInAsync(principal);
        	return RedirectToAction("Index", "Home");
    	}
    	else
    	{
            TempData["UserLoginFailed"] = "Login Failed.Please enter correct credentials";
        	return View(request);
    	}
	}
	return View(request);
}

				
			

Now when you try to login you will be redirected to the 2FA page. Here you must put code from your Authenticator App and click the Validate button. Login will be successful.

Two Factor validation

If you face any challenges or complexities while implementing two-factor authentication or any other .NET development tasks, Hire Dedicated .NET Developers for Your Project from Vivasoft.

We offer expert services to your specific requirements, ensuring your project is delivered with the highest quality and efficiency.

Contact us today to discuss your needs, and let us provide you with a top notch solution.

50+ companies rely on our top 1% talent to scale their dev teams.
Excellence Our minimum bar.
It has become a prerequisite for companies to develop custom software.
We've stopped counting. Over 50 brands count on us.
Our company specializes in software outsourcing and provides robust, scalable, and efficient solutions to clients around the world.
klikit

Chris Withers

CEO & Founder, Klikit

Klikit-logo
Heartfelt appreciation to Vivasoft Limited for believing in my vision. Their talented developers can take any challenges against all odds and helped to bring Klikit into life.appreciation to Vivasoft Limited for believing in my vision. Their talented developers can take any challenges.
Start with a dedicated squad in 7 days

NDA first, transparent rates, agile delivery from day one.

Where We Build the Future
Scale Engineering Without the Overhead

Elastic offshore teams that integrate with your processes and timezone.

Tech Stack
0 +
Blogs You May Love

Don’t let understaffing hold you back. Maximize your team’s performance and reach your business goals with the best IT Staff Augmentation

let's build our future together

Get to Know Us Better

Explore our expertise, projects, and vision.