How to Implement CAPTCHA in ASP.NET Core

How to Implement CAPTCHA in ASP.NET Core

In this article, I will discuss How to Implement CAPTCHA in ASP.NET Core with an Example. Please read our previous article discussing How to Implement the Password Expiration Policy in ASP.NET Core Identity.

What is CAPTCHA?

CAPTCHA stands for Completely Automated Public Turing Test to Tell Computers and Humans Apart. It is a security mechanism designed to differentiate between human users and automated bots accessing web applications or services.

A CAPTCHA typically appears as a challenge on web forms or login pages, requiring users to perform a task, such as typing distorted text, selecting specific images, or solving a puzzle that is easy for humans to solve but hard for bots or automated scripts. So, CAPTCHA helps protect websites from various types of malicious activities.

Why Do We Need CAPTCHA Services in Web Applications?

CAPTCHA is essential for enhancing the security of web applications. The following are the primary reasons for its use:

  • Prevent Automated Abuse: Without CAPTCHA, malicious bots could repeatedly submit forms, create fake accounts, post spam comments, or attempt brute-force logins. CAPTCHAs handle this automated abuse by introducing a difficult step for bots to solve.
  • Protect System Resources: Each request to a web application consumes bandwidth and computational resources on the server. By filtering out automated traffic, we save our resources for genuine users and reduce the load on our servers.
How does CAPTCHA Work?

Let us understand how Captcha works in Web Applications.

User Receives a Challenge

When a user attempts to sign up for an account, submit a form, or login to the application, the site prompts them with a CAPTCHA test. Examples:

  • Text-based CAPTCHA: Distorted letters and numbers that users must type correctly.
  • Image-based CAPTCHA: Users select certain images matching a prompt (e.g., “Select all squares with traffic lights”).

In our application, we will implement Text-based CAPTCHA, which will be displayed on the Login page as shown in the image below:

How to Implement CAPTCHA in ASP.NET Core with an Example

User Submits the Solution

The user answers or completes the puzzle (typing in text or selecting images). In our example, it is a text-based image. So, the user has to read the CAPTCHA image and enter the text in the CAPTCHA text box as shown in the below image:

How to Implement CAPTCHA in ASP.NET Core with an Example

Validation on Server

The user’s response is sent back to the server or a CAPTCHA verification service. The server or third-party CAPTCHA service checks if the response is correct. If the response is correct (or user behavior is deemed human), the user proceeds. If not, the request is blocked or flagged as suspicious.

How to Implement CAPTCHA in ASP.NET Core

We need to add image-based CAPTCHA functionality to our login process. Let us understand the step-by-step process of how we can integrate the CAPTCHA feature into our Local Login process.

Installing Required Packages:

First, we need to install the System.Drawing.Common Package. This package provides support for creating and manipulating images, which is essential for generating CAPTCHA images dynamically. So, please install the package by executing the following command in the Package Manager Console:

Install-Package System.Drawing.Common

  • The System.Drawing.Common package provides access to GDI+ graphics functionality. It’s essential for creating and manipulating images programmatically.
  • By installing this package, we can generate CAPTCHA images dynamically, customize their appearance, and convert them to byte arrays for transmission.
Add a CAPTCHA Service

This service is responsible for generating the CAPTCHA code and creating the image representation of the CAPTCHA. So, please create a class file named CaptchaService.cs within the Services folder and copy and paste the following code. The following code is self-explained, so please read the comment lines for a better understanding.

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Text;

namespace ASPNETCoreIdentityDemo.Services
{
    public static class CaptchaService
    {
        private static readonly char[] chars =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray();

        //If you want to inlcude Only Upper Case as part of Captcha, please use below
        //private static readonly char[] chars =
        //    "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();

        // Generates a random captcha string of the specified length.
        // length: The length of the random string, default is 6
        // returns: Randomly generated string
        public static string GenerateCaptchaCode(int length = 6)
        {
            var random = new Random();
            var sb = new StringBuilder();
            for (int i = 0; i < length; i++)
            {
                sb.Append(chars[random.Next(chars.Length)]);
            }
            return sb.ToString();
        }

        // Creates a captcha image from the given captcha text.
        // captchaCode: The text to render in the captcha image.
        // returns: A byte array representing the captcha image in PNG format.
        public static byte[] GenerateCaptchaImage(string captchaCode)
        {
            // Defines the dimensions (150x50 pixels) and initializes a Bitmap and Graphics object with anti-aliasing and high-quality settings.
            int width = 150;
            int height = 50;

            using var bitmap = new Bitmap(width, height);
            using var graphics = Graphics.FromImage(bitmap);
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.Clear(Color.White);

            // Draws each character of the CAPTCHA string onto the image with slight random rotations and color variations to enhance security.
            var random = new Random();
            using var font = new Font("Arial", 20, FontStyle.Bold);
            using var brush = new SolidBrush(Color.Black);

            // Adds random lines to the image to make automated reading more difficult.
            float x = 10f;
            for (int i = 0; i < captchaCode.Length; i++)
            {
                // Slight rotation
                graphics.ResetTransform();
                var angle = random.Next(-10, 10);
                graphics.RotateTransform(angle);

                // Random color (optional)
                brush.Color = Color.FromArgb(
                    random.Next(50, 150),
                    random.Next(50, 150),
                    random.Next(50, 150));

                // Draw each character
                graphics.DrawString(captchaCode[i].ToString(), font, brush, x, 10);
                x += 20;  // shift x a bit for the next character
            }

            // Adds random lines to the image to make automated reading more difficult.
            for (int i = 0; i < 10; i++)
            {
                var pen = new Pen(Color.FromArgb(
                    random.Next(100, 255),
                    random.Next(100, 255),
                    random.Next(100, 255)));
                var startPoint = new Point(random.Next(width), random.Next(height));
                var endPoint = new Point(random.Next(width), random.Next(height));
                graphics.DrawLine(pen, startPoint, endPoint);
            }

            // Saves the bitmap as a PNG image and converts it to a byte array for transmission.
            using var ms = new System.IO.MemoryStream();
            bitmap.Save(ms, ImageFormat.Png);
            return ms.ToArray();
        }
    }
}
Code Explanations:

The CaptchaService is a static class that provides methods to generate random CAPTCHA codes and generate corresponding CAPTCHA images.

  • GenerateCaptchaCode(int length = 6): This method generates a random CAPTCHA code consisting of alphanumeric characters. The length of the CAPTCHA string can be customized (default length = 6). The method uses a Random object to pick random characters from a predefined character array.
  • GenerateCaptchaImage(string captchaCode): This method generates a CAPTCHA image based on the provided CAPTCHA code. It uses the System.Drawing namespace to create an image and render the CAPTCHA code onto the image. Additional features like random character rotation, color variation, and noise lines are added to enhance CAPTCHA’s security and visual complexity.
Disable Camel Case in Program.cs

By default, ASP.NET Core uses a camel case for JSON property names. To maintain the same casing as in the C# properties, we need to disable the camel case in Program.cs. This is crucial for consistent property naming between the server and client, especially when dealing with JSON Data (e.g., CacheId remains CacheId instead of becoming cacheId). In your Program.cs, add the AddJsonOptions method to set the PropertyNamingPolicy to null as follows. This will affect all JSON serialization in our application.

builder.Services
    .AddControllersWithViews()
    .AddJsonOptions(options =>
    {
        // This stops System.Text.Json from converting property names to camelCase
        options.JsonSerializerOptions.PropertyNamingPolicy = null;
    });
Creating Captcha Controller

Create a new Empty MVC Controller named CaptchaController within the Controllers folder and then copy and paste the following code. This Controller handles HTTP requests related to CAPTCHA generation and refreshing. This includes generating new CAPTCHA codes, creating corresponding images, storing CAPTCHA codes in memory, and providing endpoints for the front end to retrieve and refresh CAPTCHA images.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using ASPNETCoreIdentityDemo.Services;
using System;

namespace ASPNETCoreIdentityDemo.Controllers
{
    [Route("captcha")]
    public class CaptchaController : Controller
    {
        private readonly IMemoryCache _cache;

        public CaptchaController(IMemoryCache cache)
        {
            _cache = cache;
        }

        // GET /captcha/generate
        [HttpGet("generate")]
        public IActionResult GenerateCaptcha()
        {
            // Generate random CAPTCHA code
            var captchaCode = CaptchaService.GenerateCaptchaCode(6);

            // Create a new CaptchaId
            var CaptchaId = Guid.NewGuid().ToString();

            // Store the code in memory for 10 mins (adjust as needed)
            _cache.Set(CaptchaId, captchaCode, TimeSpan.FromMinutes(10));

            // Generate the image
            var captchaImageBytes = CaptchaService.GenerateCaptchaImage(captchaCode);

            // Convert to Base64 for <img src="data:image/png;base64,..." />
            var base64Image = Convert.ToBase64String(captchaImageBytes);

            // Return JSON: { CaptchaId, CaptchaImage } 
            return Json(new
            {
                CaptchaId = CaptchaId,
                CaptchaImage = $"data:image/png;base64,{base64Image}"
            });
        }

        // GET /captcha/refresh?CaptchaId=<your-guid-here>
        [HttpGet("refresh")]
        public IActionResult RefreshCaptcha(string CaptchaId)
        {
            if (string.IsNullOrEmpty(CaptchaId))
            {
                return BadRequest("CaptchaId is required.");
            }

            // Remove existing captcha code from cache
            _cache.Remove(CaptchaId);

            // Generate a new code
            var newCaptchaCode = CaptchaService.GenerateCaptchaCode(6);

            // Store it in memory
            _cache.Set(CaptchaId, newCaptchaCode, TimeSpan.FromMinutes(10));

            // Generate the new image
            var captchaImageBytes = CaptchaService.GenerateCaptchaImage(newCaptchaCode);
            var base64Image = Convert.ToBase64String(captchaImageBytes);

            // Return JSON
            return Json(new
            {
                CaptchaId = CaptchaId,
                CaptchaImage = $"data:image/png;base64,{base64Image}"
            });
        }
    }
}
Add In-Memory Caching:

Enable in-memory caching to store CAPTCHA codes temporarily on the server’s Main memory. This ensures that the CAPTCHA validation process can verify user input against the generated code within a specified timeframe. So, please add the following code to the Program class.

builder.Services.AddMemoryCache();

Uses of In-Memory Caching: The IMemoryCache allows the application to store data in memory, which is fast and efficient for temporary data like CAPTCHA codes. The CaptchaController uses IMemoryCache to store and retrieve CAPTCHA codes based on unique identifiers (CaptchaId).

Modifying LoginViewModel (Add Captcha Fields)

We need to extend the existing login form model to include fields necessary for CAPTCHA validation. This ensures that the user’s input for CAPTCHA can be captured and validated during the login process. So, please modify the LoginViewModel as follows:

using Microsoft.AspNetCore.Authentication;
using System.ComponentModel.DataAnnotations;
namespace ASPNETCoreIdentityDemo.Models
{
    public class LoginViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Display(Name = "Remember Me")]
        public bool RememberMe { get; set; }

        public string? ReturnUrl { get; set; }

        // AuthenticationScheme is in Microsoft.AspNetCore.Authentication namespace
        public IList<AuthenticationScheme>? ExternalLogins { get; set; }

        // Captcha fields
        [Required(ErrorMessage = "Please enter the CAPTCHA text.")]
        [Display(Name = "Enter CAPTCHA")]
        // 1) Must be exactly 6 length
        // 2) Must be alphanumeric (case-insensitive)
        [StringLength(6, MinimumLength = 6, ErrorMessage = "The CAPTCHA code must be 6 alphanumeric characters.")]
        [RegularExpression("^[A-Za-z0-9]{6}$", ErrorMessage = "CAPTCHA must be exactly 6 alphanumeric characters.")]
        public string CaptchaCode { get; set; }

        // CaptchaId from Captcha
        public string? CaptchaId { get; set; }
    }
}
Login View (Add Captcha UI Elements)

Next, we need to integrate CAPTCHA UI elements into the login form, allowing users to view the CAPTCHA image, refresh it if necessary, and input the CAPTCHA code. This ensures that each login attempt includes a CAPTCHA verification step. So, please modify the Login.cshtml view as follows:

@model LoginViewModel
@{
ViewBag.Title = "Login";
}
<div class="container my-5">
<div class="row">
<!-- Local Account Login -->
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Local Account Login</h4>
</div>
<div class="card-body">
<form method="post" asp-action="Login" asp-controller="Account" novalidate>
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />
<!-- Validation Summary -->
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<!-- Email Field -->
<div class="mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" placeholder="Enter your email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<!-- Password Field -->
<div class="mb-3">
<label asp-for="Password" class="form-label"></label>
<input asp-for="Password" class="form-control" placeholder="Enter your password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<!-- Captcha Section -->
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<!-- We'll dynamically set the 'src' from JS -->
<img id="captcha-image" alt="captcha" style="border: 1px solid #ccc; margin-right: 1em;" />
<button type="button" class="btn btn-link" id="refresh-captcha">Refresh</button>
</div>
<!-- Hidden field for CaptchaId -->
<input asp-for="CaptchaId" type="hidden" id="CaptchaId" />
<label asp-for="CaptchaCode" class="form-label"></label>
<input asp-for="CaptchaCode" class="form-control" placeholder="Enter the text shown above" />
<span asp-validation-for="CaptchaCode" class="text-danger"></span>
</div>
<!-- Remember Me -->
<div class="form-check mb-3">
<input asp-for="RememberMe" class="form-check-input" />
<label asp-for="RememberMe" class="form-check-label">
@Html.DisplayNameFor(m => m.RememberMe)
</label>
</div>
<!-- Login Button & Confirm Email Button -->
<div class="d-flex justify-content-between align-items-center">
<button type="submit" class="btn btn-success">Log In</button>
<a class="btn btn-warning"
asp-controller="Account"
asp-action="ResendConfirmationEmail"
asp-route-IsResend="false">Confirm Email</a>
</div>
</form>
<!-- Register & Forgot Password Options -->
<div class="mt-3 d-flex justify-content-end">
<a asp-controller="Account" asp-action="Register" class="btn btn-link">
Register
</a>
<a asp-controller="Account" asp-action="ForgotPassword" class="btn btn-link">
Forgot Password?
</a>
</div>
</div>
</div>
</div>
<!-- External Login -->
<div class="col-md-6 mt-4 mt-md-0">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">External Login</h4>
</div>
<div class="card-body">
@if (Model.ExternalLogins == null || Model.ExternalLogins.Count == 0)
{
<div class="alert alert-info mb-0">
No external logins configured
</div>
}
else
{
<p class="mb-3">You can log in using one of the following providers:</p>
<div class="d-grid gap-2">
@foreach (var provider in Model.ExternalLogins)
{
<button type="button"
class="btn btn-outline-primary external-login-btn"
data-provider="@provider.Name"
data-url="@Url.Action("ExternalLogin", "Account", new { provider = provider.Name, returnUrl = Model.ReturnUrl })"
title="Log in using your @provider.DisplayName account">
@provider.DisplayName
</button>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
// On page load, fetch a new Captcha
document.addEventListener('DOMContentLoaded', () => {
fetchCaptcha();
});
// Handle Refresh button
document.getElementById('refresh-captcha').addEventListener('click', () => {
const CaptchaId = document.getElementById('CaptchaId').value;
if (!CaptchaId) {
// If we don't have a CaptchaId yet, just do a fresh fetch
fetchCaptcha();
} else {
refreshCaptcha(CaptchaId);
}
});
// Fetch a fresh captcha from /captcha/generate
function fetchCaptcha() {
fetch('/captcha/generate')
.then(response => response.json())
.then(data => {
// data = { CaptchaId, CaptchaImage }
document.getElementById('captcha-image').src = data.CaptchaImage;
document.getElementById('CaptchaId').value = data.CaptchaId;
})
.catch(err => console.error('Captcha fetch error:', err));
}
// Refresh an existing captcha by reusing the same CaptchaId
function refreshCaptcha(CaptchaId) {
fetch('/captcha/refresh?CaptchaId=' + CaptchaId)
.then(response => response.json())
.then(data => {
document.getElementById('captcha-image').src = data.CaptchaImage;
// Possibly the same CaptchaId is returned, but we’ll re-assign anyway
document.getElementById('CaptchaId').value = data.CaptchaId;
// Clear the captcha text field
document.getElementById('CaptchaCode').value = '';
})
.catch(err => console.error('Captcha refresh error:', err));
}
// External Login
document.querySelectorAll('.external-login-btn').forEach(button => {
button.addEventListener('click', function () {
var provider = this.getAttribute('data-provider');
var url = this.getAttribute('data-url');
// Define popup window size
var width = 500;
var height = 600;
var left = (window.innerWidth / 2) - (width / 2);
var top = (window.innerHeight / 2) - (height / 2);
// Open the popup window
var popup = window.open(url, provider, `width=${width},height=${height},top=${top},left=${left},resizable=yes`);
// Monitor the popup to check if it closes
var checkPopup = setInterval(function () {
if (popup.closed) {
clearInterval(checkPopup);
// Optionally, reload or fetch updated user state
location.reload(); // Refresh the parent window to reflect login state
}
}, 500); // Check every 500ms if the popup is closed
});
});
</script>
}
JavaScript for Dynamic CAPTCHA Handling

JavaScript is Embedded within the @section Scripts of Login.cshtml view. It manages the dynamic aspects of CAPTCHA, such as fetching a new CAPTCHA on page load, refreshing the CAPTCHA image upon user request, and handling the associated CaptchaId.

fetchCaptcha:
  • Sends a GET request to /captcha/generate to obtain a new CAPTCHA.
  • Updates the src attribute of the CAPTCHA image with the Base64-encoded image.
  • Sets the CaptchaId hidden field with the unique identifier returned from the server.
refreshCaptcha:
  • Sends a GET request to /captcha/refresh with the existing CaptchaId to obtain a new CAPTCHA image without changing the ID.
  • Updates the CAPTCHA image and maintains the same CaptchaId.
  • Clears the previous CAPTCHA code input field to prevent confusion.
Event Listeners:
  • DOMContentLoaded: Triggers fetchCaptcha when the page loads.
  • Refresh Button: Triggers refreshCaptcha when the user clicks the “Refresh” button.
Inject Memory Cache into Account Controller:

Next, we need to Inject the Memory Cache instance into the Account Controller as follows:

public class AccountController : Controller
{
//userManager will hold the UserManager instance
private readonly UserManager<ApplicationUser> userManager;
//signInManager will hold the SignInManager instance
private readonly SignInManager<ApplicationUser> signInManager;
//emailSender will hold the EmailSenderService instance
private readonly EmailSenderService emailSender;
//smsSender will hold the SMSSender instance
private readonly SMSSender smsSender;
//_context hold the ApplicationDbContext instance
private readonly ApplicationDbContext _context;
//_memoryCache hold the MemoryCache instance
private readonly IMemoryCache _memoryCache;
// UserManager, SignInManager and EmailSenderService services are injected into the AccountController
// using constructor injection
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager,
EmailSenderService emailSender, SMSSender smsSender, ApplicationDbContext context,
IMemoryCache memoryCache)
{
this.userManager = userManager;
this.signInManager = signInManager;
this.emailSender = emailSender;
this.smsSender = smsSender;
_context = context;
_memoryCache = memoryCache;
}
//Rest of all Code
}
Modifying Login Post Action Method:

Next, we need to integrate CAPTCHA validation into the login process. This involves verifying that the user has entered the correct CAPTCHA code before authentication. When redisplaying the form after a failed CAPTCHA validation, it’s essential to clear the previous CaptchaCode from the model and ModelState to prevent the old input from being shown again. So, please modify the Login Post action method as follows.

[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
// 1. Retrieve the cached captcha code from memory
if (string.IsNullOrWhiteSpace(model.CaptchaId)
|| !_memoryCache.TryGetValue(model.CaptchaId, out string? storedCaptchaCode))
{
ModelState.AddModelError("", "Captcha has expired or is invalid. Please refresh and try again.");
// Clear the user's CaptchaCode property in the model
model.CaptchaCode = string.Empty;
// ALSO remove the CaptchaCode entry from ModelState so it won't rebind 
// the old posted value into the form field.
ModelState.Remove(nameof(model.CaptchaCode));
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}
// 2. Compare codes
if (storedCaptchaCode != model.CaptchaCode)
{
ModelState.AddModelError("", "Invalid CAPTCHA. Please try again.");
// Clear the user's CaptchaCode property in the model
model.CaptchaCode = string.Empty;
// ALSO remove the CaptchaCode entry from ModelState so it won't rebind 
// the old posted value into the form field.
ModelState.Remove(nameof(model.CaptchaCode));
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}
// 3. Remove it from memory
_memoryCache.Remove(model.CaptchaId);
// Clear the user's CaptchaCode property in the model
model.CaptchaCode = string.Empty;
// ALSO remove the CaptchaCode entry from ModelState so it won't rebind 
// the old posted value into the form field.
ModelState.Remove(nameof(model.CaptchaCode));
// Proceed with the existing login logic
// Find user by email
var user = await userManager.FindByEmailAsync(model.Email);
if (user == null)
{
// Generic error message to avoid disclosing details
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}
// Check if the user is locked out
if (await userManager.IsLockedOutAsync(user))
{
// Inform the user that their account is locked
ModelState.AddModelError(string.Empty, "Your account is locked due to multiple unsuccessful login attempts. Please try again later or contact support.");
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}
// Check Password first to avoid leaking information about Email Confirmation
var passwordCheck = await userManager.CheckPasswordAsync(user, model.Password);
if (!passwordCheck)
{
// Increment access failed count
await userManager.AccessFailedAsync(user);
// Calculate remaining attempts
var accessFailedCount = await userManager.GetAccessFailedCountAsync(user); //Return: 0
var maxFailedAccessAttempts = userManager.Options.Lockout.MaxFailedAccessAttempts; // 5
var attemptsLeft = maxFailedAccessAttempts - accessFailedCount; // 5
// Provide feedback to the user
if (accessFailedCount != 0)
{
ModelState.AddModelError(string.Empty, $"Invalid login attempt. You have {attemptsLeft} more {(attemptsLeft == 1 ? "attempt" : "attempts")} before your account gets locked.");
}
else
{
// Handle lockout scenario
await SendAccountLockedEmail(user);
return RedirectToAction("AccountLocked", "Account");
}
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}
// Password is valid, now check email confirmation
if (!user.EmailConfirmed)
{
ModelState.AddModelError(string.Empty, "Your email address is not confirmed. Please confirm your email to log in.");
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}
// SignIn the User
// The last boolean parameter lockoutOnFailure indicates if the account should be locked on failed login attempt. 
// On every failed login attempt AccessFailedCount column value in AspNetUsers table is incremented by 1. 
// When the AccessFailedCount reaches the configured MaxFailedAccessAttempts which in our case is 5,
// the account will be locked and LockoutEnd column will be populated.
var result = await signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
//Check Password Expiration
//Here we are hardcoding days as 90
if (user.LastPasswordChangedDate.AddDays(90) < DateTime.Now)
{
// Password has expired
// SignOut the user and redirect to PasswordExpired Pae
await signInManager.SignOutAsync();
return View("PasswordExpired", "Account");
}
// Redirect user after successful sign-in
if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
else if (result.RequiresTwoFactor)
{
// Handle two-factor authentication
// Generate a 2FA token either using DefaultPhoneProvider or DefaultEmailProvider
// Which provider we use here, same we need to use while doing the verification
var TwoFactorAuthenticationToken = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
//var TwoFactorAuthenticationToken3 = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);
// Send SMS if the phone number is confirmed
if (user.PhoneNumberConfirmed && !string.IsNullOrEmpty(user.PhoneNumber))
{
var smsMessage = $"Your Two-Factor Authentication code is: {TwoFactorAuthenticationToken}. Please use this code to log in.";
await smsSender.SendSmsAsync(user.PhoneNumber, smsMessage);
}
// Send Email if the email is confirmed
if (user.EmailConfirmed && !string.IsNullOrEmpty(user.Email))
{
var emailSubject = "Two-Factor Authentication Code";
var emailBody = $@"
<p>Hello {user.FirstName} {user.LastName},</p>
<p>Your Two-Factor Authentication code is: <strong>{TwoFactorAuthenticationToken}</strong></p>
<p>If you did not request this code, please contact our support team immediately.</p>
<p>Thank you,<br/>Your Dot Net Tutorials Team</p>";
await emailSender.SendEmailAsync(user.Email, emailSubject, emailBody, IsBodyHtml: true);
}
// Redirect to Two-Factor Authentication verification page with data
return RedirectToAction("VerifyTwoFactorToken", "Account", new { model.Email, model.ReturnUrl, model.RememberMe });
}
else if (result.IsLockedOut)
{
// Handle lockout scenario
// It's important to inform users when their account is locked.
await SendAccountLockedEmail(user);
return RedirectToAction("AccountLocked", "Account");
}
else
{
// Generic error message for invalid credentials. Also displaying the number of attempts left
var attemptsLeft = userManager.Options.Lockout.MaxFailedAccessAttempts - await userManager.GetAccessFailedCountAsync(user);
ModelState.AddModelError(string.Empty, $"Invalid login attempt. You have {attemptsLeft} more {(attemptsLeft == 1 ? "attempt" : "attempts")} before your account gets locked.");
return View(model);
}
}
// If we got this far, something failed; redisplay the form
model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
return View(model);
}

Note: When an ASP.NET Core form is re-displayed after a failed validation, the Razor engine re-populates form fields from the ModelState (not just your model object). That’s why even if we set the model.CaptchaCode = string.Empty;, the old value can still show up in the input field. So, we need to remove or update the ModelState entry for CaptchaCode (ModelState.Remove(nameof(model.CaptchaCode))) right after we set the model.CaptchaCode = string.Empty;

CAPTCHA services are essential in safeguarding web applications against an automated boa. By effectively distinguishing between human users and bots, CAPTCHA ensures online web applications’ integrity, security, and smooth functioning.

In the next article, I will discuss How to Edit and View User Details in ASP.NET Core Identity. In this article, I explain How to Implement Captcha in ASP.NET Core Identity. I hope you enjoy this article on How to Implement Captcha in ASP.NET Core Identity.

Leave a Reply

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