Two factor authentication
Two factor authentication

Two-Factor Authentication (2FA) with OTP in ASP.NET Core 6

Introduction

Nowadays, authentication is crucial for everyone due to security concerns. Traditionally, we used a username and password to verify user identity. While this approach is still common, many systems now implement an additional layer of security known as two-factor authentication. Two-Factor Authentication (2FA) adds an extra layer of security to user authentication by requiring not only a password and username but also something that only the user has on them.

in this blog we implementing 2 factor authentication based on OTP (One-Time Password) in an ASP.NET .Core application.

Prerequisites

  • Need Basic knowledge of ASP.NET Core and MVC
  • An existing ASP.NET Core web application
  • Need An email service setup to send OTP emails

Step 1: Set Up Your ASP.NET Core Project

in this step you need set up asp .net project to implement 2 factor authentication.

Step 2: Create Your Models

Create a view model for the login process and otp process.

   public class LoginViewModel
   {
       [Required]
       [EmailAddress]
       public string? Email { get; set; }

       [Required]
       public string? Password { get; set; }

       public bool RememberMe { get; set; }
   }

OtpViewModel.cs:

    public class OtpViewModel
    {
        // The OTP entered by the user
        [Required(ErrorMessage = "OTP is required.")]
        [StringLength(6, ErrorMessage = "OTP must be 6 digits long.", MinimumLength = 6)]
        [Display(Name = "Enter OTP")]
        public string Otp { get; set; }

        // The email of the user (optional, depending on your flow)
        [Required(ErrorMessage = "Email is required.")]
        [EmailAddress(ErrorMessage = "Invalid email format.")]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }

Step 3: Update Your HomeController

In this step We’ll implement the logic for login and OTP verification in the HomeController.

  1. Add Dependencies: Ensure you have the necessary using directives:
using Emp.DAL.Data;
using EMS.Web.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using EMS.Web.Utility;

Modify the HomeController: in thise you need to modified home controller for Implement the login logic, OTP generation, and email sending.

   public class HomeController : Controller
   {
       private readonly ILogger<HomeController> _logger;
       private readonly EmsContext _ems;
       private readonly IWebHostEnvironment _webHostEnvironment;
       public HomeController(ILogger<HomeController> logger, EmsContext ems, IWebHostEnvironment webHostEnvironment)
       {
           _logger = logger;
           _ems = ems;
           _webHostEnvironment = webHostEnvironment;
       }

       public IActionResult Index()
       {
           return View();
       }
       public IActionResult Test()
       {
           return View();
       }

       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 });
       }
       public IActionResult Login()
       {
           return View();
       }
       public async Task<IActionResult> SaveLogin(LoginViewModel model)
       {
           if (!ModelState.IsValid)
           {
               TempData["ErrorMessage"] = "Please fill out all fields correctly.";
               return RedirectToAction("Login"); // Return the same view with validation errors
           }

           var employee = await _ems.Employees.FirstOrDefaultAsync(e => e.Email == model.Email && e.Active == true);
           if (employee == null)
           {
               TempData["ErrorMessage"] = "Invalid login attempt. Email not found.";
               return RedirectToAction("Login");
           }

           if (employee.IsLockedOut == true)
           {
               if (employee.LockoutEndTime.HasValue && employee.LockoutEndTime.Value > DateTime.Now)
               {
                   var timeLeft = employee.LockoutEndTime.Value - DateTime.Now;
                   var minutesLeft = (int)Math.Ceiling(timeLeft.TotalMinutes);
                   TempData["ErrorMessage"] = $"Your account has been temporarily locked due to multiple unsuccessful login attempts. Please try again in {minutesLeft} minutes. If you need immediate assistance, contact your administrator.";
                   return RedirectToAction("Login");
               }
               else
               {
                   employee.IsLockedOut = false;
                   employee.FailedLoginAttempts = 0;
                   employee.LockoutEndTime = null;
               }
           }

           if (employee.TempPassword != model.Password)
           {
               employee.FailedLoginAttempts++;

               if (employee.FailedLoginAttempts >= 3)
               {
                   employee.IsLockedOut = true;
                   employee.LockoutEndTime = DateTime.Now.AddMinutes(15);
               }

               await _ems.SaveChangesAsync();
               TempData["ErrorMessage"] = "Invalid login attempt. Your account may be locked after multiple attempts.";
               return RedirectToAction("Login");
           }

           // Successful temporary password authentication
           employee.FailedLoginAttempts = 0;
           employee.IsLockedOut = false;
           employee.LockoutEndTime = null;
           await _ems.SaveChangesAsync();

           // Generate and send OTP
           string otp = GenerateOtp();
           await SendOtp(employee.Email, otp);

           // Store OTP in TempData or session
           HttpContext.Session.SetString("Otp", otp);  // Store OTP in session

           // Set success message
           TempData["SuccessMessage"] = "Login successful. Please enter the OTP sent to your registered email.";

           // Redirect to OTP verification page
           return RedirectToAction("Otp", "Home"); // Ensure you have an OTP action in your Account controller
       }
       public IActionResult Otp()
       {
           return View();
       }
       // Example method for generating OTP
       private string GenerateOtp()
       {
           // Implement OTP generation logic (e.g., random number, time-based, etc.)
           Random random = new Random();
           return random.Next(100000, 999999).ToString(); // Example: 6-digit OTP
       }

       // Example method for sending OTP
       private async Task SendOtp(string email, string otp)
       {
           EmailService otpemail=new EmailService();
           string subject = "Your OTP Code";
           string body = $"<p>Your OTP code is: <strong>{otp}</strong></p><p>Please use this code to complete your login.</p>";
           string logoPath = Path.Combine(_webHostEnvironment.WebRootPath, "pic", "logo.png");

           // Call the SendEmail method to send the OTP
           otpemail.SendEmail(email, subject, body, logoPath);
       }

       [HttpPost]
       public async Task<IActionResult> Otp(OtpViewModel model)
       {
           if (!ModelState.IsValid)
           {
               TempData["ErrorMessage"] = "Please enter a valid OTP.";
               return RedirectToAction("Otp"); // Return to OTP page with validation errors
           }

           // Retrieve the stored OTP from the session
           var storedOtp = HttpContext.Session.GetString("Otp");

           // Verify the OTP
           if (storedOtp == null || model.Otp != storedOtp)
           {
               TempData["ErrorMessage"] = "Invalid OTP. Please try again.";
               return RedirectToAction("Otp");
           }

           // Get the employee (you might want to pass the employee's email to this method)
           var employee = await _ems.Employees.FirstOrDefaultAsync(e => e.Email == model.Email && e.Active == true);

           if (employee != null)
           {
               // Create claims for the authenticated employee
               var claims = new List<Claim>
       {
           new Claim(ClaimTypes.Email, employee.Email ?? string.Empty),
           new Claim(ClaimTypes.NameIdentifier, employee.EmpId.ToString()),
           new Claim("FullName", $"{employee.FirstName ?? string.Empty} {employee.LastName ?? string.Empty}"),
           new Claim("Position", employee.Position ?? string.Empty),
           new Claim("EmpId", employee.EmpId.ToString()),
           new Claim("IsProfilecomplte", employee.IsProfilecomplte.ToString()),
           new Claim("Roleid", employee.Roleid.ToString()),
       };

               var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

               // Sign in the user
               await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
               var setusers = UserClaimsHelper.GetUserClaims(User);
             var roleId= setusers["Roleid"];
               // Redirect to the dashboard or welcome page
               return RedirectToAction("Welcome", "Dashboard");
           }

           // If employee not found
           TempData["ErrorMessage"] = "An error occurred. Please try again.";
           return RedirectToAction("Login");
       }

       [HttpGet]
       public IActionResult AccessDenied()
       {
           return View();
       }
       public async Task<IActionResult> Logout()
       {
           // Sign out the user
           await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

           // Optionally clear other cookies
           foreach (var cookie in Request.Cookies.Keys)
           {
               // Remove each cookie
               Response.Cookies.Delete(cookie);
           }

           // Redirect to the login page or another page
           return RedirectToAction("Login", "Home");
       }
   }

Step 4: Create Your Views

Login View (Login.cshtml): Need to create page for Login


@model EMS.Web.Models.LoginViewModel
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(to right, #FFFAFA, #FFFAFA, #ffffff);
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .login-container {
            width: 350px;
            background-color: #fff;
            border-radius: 10px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
            padding: 40px;
        }

        h2 {
            text-align: center;
            color: #333;
        }

        .login-form {
            display: flex;
            flex-direction: column;
        }

        .form-group {
            margin-bottom: 20px;
        }

        label {
            font-weight: bold;
        }

        input[type="text"],
        input[type="password"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            outline: none;
        }

        button {
            background-color: #2980b9;
            color: #fff;
            border: none;
            padding: 10px 20px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s ease;
        }

            button:hover {
                background-color: #1a5276;
            }

        .alert {
            padding: 15px;
            margin-bottom: 20px;
            border-radius: 5px;
        }

        .alert-success {
            background-color: #d4edda;
            color: #155724;
        }

        .alert-danger {
            background-color: #f8d7da;
            color: #721c24;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h2>Login</h2>

        <!-- Success Message -->
        @if (TempData["SuccessMessage"] != null)
        {
            <div class="alert alert-success">
                @TempData["SuccessMessage"]
            </div>
        }

        <!-- Error Message -->
        @if (TempData["ErrorMessage"] != null)
        {
            <div class="alert alert-danger">
                @TempData["ErrorMessage"]
            </div>
        }

        <form asp-controller="Home" asp-action="SaveLogin" class="login-form">
            <div class="form-group">
                <label asp-for="Email">Username:</label>
                <input asp-for="Email" type="text" class="form-control" id="Email" name="Email" required>
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password">Password:</label>
                <input asp-for="Password" type="password" class="form-control" id="password" name="password" required>
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label>
                    <input asp-for="RememberMe" type="checkbox"> Remember Me
                </label>
            </div>
            <button type="submit" class="btn btn-primary">Login</button>
        </form>
    </div>
</body>
</html>

OTP View (Otp.cshtml):

@model EMS.Web.Models.OtpViewModel

@{
ViewData["Title"] = "Otp";
Layout = null;
}


<form asp-action="Otp" asp-controller="Home" method="post">
<div>
<label asp-for="Email"></label>
<input asp-for="Email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div>
<label asp-for="Otp"></label>
<input asp-for="Otp" />
<span asp-validation-for="Otp" class="text-danger"></span>
</div>
<button type="submit">Verify OTP</button>
</form>



Step 5: Configure Sessions in Startup

Ensure you have session services configured in your Startup.cs or Program file:

/ Add services to the container.
builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation();
builder.Services.AddScoped();
builder.Services.AddDbContext
(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Home/Login"; // Redirect to login page if not authenticated
options.AccessDeniedPath = "/Home/AccessDenied"; // Redirect if access is denied
});

builder.Services.AddAuthorization(options =>
{
// Configure other authorization policies if necessary
});
// Add session services
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30); // Set session timeout
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true; // Essential for GDPR compliance
});
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseSession(); // Enable session before UseAutho
app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Login}/{id?}");

app.Run();

Use the Email Service in Your Application:

you need to implement email service on your application to handling email

using System.Net.Mail;
using System.Net.Mime;
using System.Net;

namespace EMS.Web.Utility
{
    public class EmailService
    {
        public void SendEmail(string toEmail, string subject, string body, string logoPath)
        {
            var fromAddress = new MailAddress("test@gmail.com", "name");
            var toAddress = new MailAddress(toEmail);
            string fromPassword = "passcode"; // Use an app-specific password if 2FA is enabled
            string smtpHost = "smtp.gmail.com";
            int smtpPort = 587;

            var smtp = new SmtpClient
            {
                Host = smtpHost,
                Port = smtpPort,
                EnableSsl = true,
                DeliveryMethod = SmtpDeliveryMethod.Network,
                UseDefaultCredentials = false,
                Credentials = new NetworkCredential(fromAddress.Address, fromPassword)
            };

            using (var message = new MailMessage(fromAddress, toAddress)
            {
                Subject = subject,
                IsBodyHtml = true
            })
            {
                // Create the HTML view
                AlternateView htmlView = AlternateView.CreateAlternateViewFromString(body, null, MediaTypeNames.Text.Html);

                // Add the logo as a linked resource
                LinkedResource logo = new LinkedResource(logoPath, MediaTypeNames.Image.Jpeg)
                {
                    ContentId = "LogoImage"
                };
                htmlView.LinkedResources.Add(logo);

                // Add the HTML view to the message
                message.AlternateViews.Add(htmlView);

                smtp.Send(message);
            }
        }
     
    }
}

Conclusion:

with these step you can easily use two factor authentication or MFA in your system .Users will now authenticate with their credentials and receive an OTP via email to complete their login.

please visit for more details apitpoint

featured image freepik.com

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *