Edi.Captcha 5.0.1

Edi.Captcha.AspNetCore

The Captcha module used in my blog

.NET

NuGet

Install

NuGet Package Manager

Install-Package Edi.Captcha

or .NET CLI

dotnet add package Edi.Captcha

Session-Based Captcha (Traditional Approach)

1. Register in DI

services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
});

services.AddSessionBasedCaptcha();
// Don't forget to add this line in your `Configure` method.
 app.UseSession();

or you can customize the options

services.AddSessionBasedCaptcha(option =>
{
    option.Letters = "2346789ABCDEFGHJKLMNPRTUVWXYZ";
    option.SessionName = "CaptchaCode";
    option.CodeLength = 4;
});

2. Generate Image

Using MVC Controller

private readonly ISessionBasedCaptcha _captcha;

public SomeController(ISessionBasedCaptcha captcha)
{
    _captcha = captcha;
}

[Route("get-captcha-image")]
public IActionResult GetCaptchaImage()
{
    var s = _captcha.GenerateCaptchaImageFileStream(
        HttpContext.Session,
        100,
        36
    );
    return s;
}

Using Middleware

app.UseSession().UseCaptchaImage(options =>
{
    options.RequestPath = "/captcha-image";
    options.ImageHeight = 36;
    options.ImageWidth = 100;
});

3. Add CaptchaCode Property to Model

[Required]
[StringLength(4)]
public string CaptchaCode { get; set; }

5. View

<div class="col">
    <div class="input-group">
        <div class="input-group-prepend">
            <img id="img-captcha" src="~/captcha-image" />
        </div>
        <input type="text" 
               asp-for="CommentPostModel.CaptchaCode" 
               class="form-control" 
               placeholder="Captcha Code" 
               autocomplete="off" 
               minlength="4"
               maxlength="4" />
    </div>
    <span asp-validation-for="CommentPostModel.CaptchaCode" class="text-danger"></span>
</div>

6. Validate Input

_captcha.ValidateCaptchaCode(model.CommentPostModel.CaptchaCode, HttpContext.Session)

To make your code look more cool, you can also write an Action Filter like this:

public class ValidateCaptcha : ActionFilterAttribute
{
    private readonly ISessionBasedCaptcha _captcha;

    public ValidateCaptcha(ISessionBasedCaptcha captcha)
    {
        _captcha = captcha;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var captchaedModel =
            context.ActionArguments.Where(p => p.Value is ICaptchable)
                                   .Select(x => x.Value as ICaptchable)
                                   .FirstOrDefault();

        if (null == captchaedModel)
        {
            context.ModelState.AddModelError(nameof(captchaedModel.CaptchaCode), "Captcha Code is required");
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
        else
        {
            if (!_captcha.Validate(captchaedModel.CaptchaCode, context.HttpContext.Session))
            {
                context.ModelState.AddModelError(nameof(captchaedModel.CaptchaCode), "Wrong Captcha Code");
                context.Result = new ConflictObjectResult(context.ModelState);
            }
            else
            {
                base.OnActionExecuting(context);
            }
        }
    }
}

and then

services.AddScoped<ValidateCaptcha>();

and then


public class YourModelWithCaptchaCode : ICaptchable
{
    public string YourProperty { get; set; }

    [Required]
    [StringLength(4)]
    public string CaptchaCode { get; set; }
}

[ServiceFilter(typeof(ValidateCaptcha))]
public async Task<IActionResult> SomeAction(YourModelWithCaptchaCode model)
{
    // ....
}

Advantages of Stateless Captcha:

  • ✅ Works in clustered/load-balanced environments
  • ✅ No server-side session storage required
  • ✅ Built-in expiration through encryption
  • ✅ Secure token-based validation
  • ✅ Better scalability
  • ✅ Single API call for both token and image

1. Register in DI

services.AddStatelessCaptcha();

or with custom options:

services.AddStatelessCaptcha(options =>
{
    options.Letters = "2346789ABCDGHKMNPRUVWXYZ";
    options.CodeLength = 4;
    options.TokenExpiration = TimeSpan.FromMinutes(5);
});

2. Create Model with Token Support

public class StatelessHomeModel
{
    [Required]
    [StringLength(4)]
    public string CaptchaCode { get; set; }
    
    public string CaptchaToken { get; set; }
}

3. Example Controller

using Edi.Captcha.SampleApp.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Diagnostics;

namespace Edi.Captcha.SampleApp.Controllers;

public class StatelessController(IStatelessCaptcha captcha) : Controller
{
    public IActionResult Index()
    {
        return View(new StatelessHomeModel());
    }

    [HttpPost]
    public IActionResult Index(StatelessHomeModel model)
    {
        if (ModelState.IsValid)
        {
            bool isValidCaptcha = captcha.Validate(model.CaptchaCode, model.CaptchaToken);
            return Content(isValidCaptcha ? "Success - Stateless captcha validated!" : "Invalid captcha code");
        }

        return BadRequest();
    }

    [Route("get-stateless-captcha")]
    public IActionResult GetStatelessCaptcha()
    {
        var result = captcha.GenerateCaptcha(100, 36);
        
        return Json(new { 
            token = result.Token, 
            imageBase64 = Convert.ToBase64String(result.ImageBytes)
        });
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

4. Example View

@model StatelessHomeModel
@{
    ViewData["Title"] = "Stateless Captcha Example";
}

<div class="text-center">
    <h1 class="display-4">Stateless Captcha Example</h1>
    <p>This example shows how to use stateless captcha that works in clustered environments.</p>
</div>

<div class="row">
    <div class="col-md-6 offset-md-3">
        <div class="card">
            <div class="card-header">
                <h5>Stateless Captcha Form</h5>
            </div>
            <div class="card-body">
                <form asp-action="Index" method="post" id="stateless-form">
                    <div class="form-group mb-3">
                        <label>Captcha Image:</label>
                        <div class="d-flex align-items-center">
                            <img id="captcha-image" src="" alt="Captcha" class="me-2" style="border: 1px solid #ccc;" />
                            <button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshCaptcha()">
                                🔄 Refresh
                            </button>
                        </div>
                        <small class="form-text text-muted">Click refresh to get a new captcha</small>
                    </div>

                    <div class="form-group mb-3">
                        <label asp-for="CaptchaCode">Enter Captcha Code:</label>
                        <input asp-for="CaptchaCode" class="form-control" placeholder="Enter the code from image" autocomplete="off" />
                        <span asp-validation-for="CaptchaCode" class="text-danger"></span>
                    </div>

                    <input type="hidden" asp-for="CaptchaToken" id="captcha-token" />

                    <div class="form-group">
                        <button type="submit" class="btn btn-primary">Submit</button>
                        <a asp-controller="Home" asp-action="Index" class="btn btn-secondary">Session-based Example</a>
                    </div>
                </form>

                <div class="mt-4">
                    <h6>Advantages of Stateless Captcha:</h6>
                    <ul class="small">
                        <li>✅ Works in clustered/load-balanced environments</li>
                        <li>✅ No server-side session storage required</li>
                        <li>✅ Built-in expiration through encryption</li>
                        <li>✅ Secure token-based validation</li>
                        <li>✅ Better scalability</li>
                        <li>✅ Single API call for both token and image</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
    async function refreshCaptcha() {
        try {
            const response = await fetch('/get-stateless-captcha');
            const data = await response.json();
            
            // Set the token for validation
            document.getElementById('captcha-token').value = data.token;
            
            // Set the image source using base64 data
            document.getElementById('captcha-image').src = `data:image/png;base64,${data.imageBase64}`;
            
            // Clear the input
            document.getElementById('CaptchaCode').value = '';
        } catch (error) {
            console.error('Error refreshing captcha:', error);
            alert('Failed to load captcha. Please try again.');
        }
    }

    // Initialize captcha on page load
    document.addEventListener('DOMContentLoaded', function() {
        refreshCaptcha();
    });
</script>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Showing the top 20 packages that depend on Edi.Captcha.

Packages
MoongladePure.Comments
Package Description

Version Last updated
5.3.0 11/14/2025
5.2.1 10/31/2025
5.2.0 10/15/2025
5.1.1 10/4/2025
5.1.0 10/8/2025
5.0.1 10/8/2025
5.0.0 10/8/2025
4.0.0 8/18/2025
3.26.4 7/31/2025
3.26.3 7/3/2025
3.26.2 6/7/2025
3.26.1 3/12/2025
3.26.0 4/2/2025
3.25.0 3/13/2025
3.24.0 4/30/2025
3.23.1 4/27/2025
3.23.0 5/7/2025
3.22.0 6/7/2025
3.21.2 5/13/2025
3.21.1 5/12/2025
3.21.0 6/7/2025
3.20.0 5/1/2025
3.19.1 4/25/2025
3.19.0 3/28/2025
3.18.0 5/12/2025
3.17.0 6/30/2025
3.16.0 4/28/2025
3.15.0 6/30/2025
3.14.0 5/5/2025
3.13.1 5/6/2025
3.13.0 5/9/2025
3.12.0 5/3/2025
3.11.0 6/6/2025
3.10.0 4/18/2025
3.9.0 4/30/2025
3.8.0 5/22/2025
3.7.0 5/18/2025
3.6.1 6/3/2025
3.6.0 5/13/2025
3.5.0 5/19/2025
3.4.0 5/2/2025
3.3.0 5/5/2025
3.2.0 6/4/2025
3.1.0 5/29/2025
3.0.1 5/2/2025
3.0.0 6/7/2025
2.2.0 6/8/2025
2.1.0 4/17/2025
2.0.0 5/12/2025
2.0.0-preview3 6/6/2025
2.0.0-preview2 5/11/2025
2.0.0-preview 6/10/2025
1.3.1 6/9/2025
1.3.0 5/23/2025
1.2.0 5/19/2025
1.1.0 5/4/2025
1.0.0 6/9/2025