Edi.Captcha 5.1.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 and View

See: src\Edi.Captcha.SampleApp\Controllers\StatelessController.cs and src\Edi.Captcha.SampleApp\Views\Stateless\Index.cshtml for a complete example.

Cluster/Load Balancer Configuration

⚠️ Important for Production Deployments: The stateless captcha uses ASP.NET Core's Data Protection API for token encryption. In clustered environments or behind load balancers, you must configure shared data protection keys to ensure captcha tokens can be validated on any server.

Option 1: File System (Network Share)

public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(@"\\shared-network-path\keys"))
        .SetApplicationName("YourAppName"); // Must be consistent across all instances
    
    services.AddStatelessCaptcha(options =>
    {
        // Your captcha configuration
    });
}

Option 2: Azure Blob Storage

public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToAzureBlobStorage("DefaultEndpointsProtocol=https;AccountName=...", "keys-container", "dataprotection-keys.xml")
        .SetApplicationName("YourAppName");
    
    services.AddStatelessCaptcha(options =>
    {
        // Your captcha configuration
    });
}

Option 3: Redis

public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect("your-redis-connection"), "DataProtection-Keys")
        .SetApplicationName("YourAppName");
    
    services.AddStatelessCaptcha(options =>
    {
        // Your captcha configuration
    });
}

Option 4: SQL Server

public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToDbContext<YourDbContext>()
        .SetApplicationName("YourAppName");
    
    services.AddStatelessCaptcha(options =>
    {
        // Your captcha configuration
    });
}

Single Server Deployment

For single server deployments, no additional configuration is required. The default Data Protection configuration will work correctly.

Testing Cluster Configuration

To verify your cluster configuration is working:

  1. Generate a captcha on Server A
  2. Submit the form to Server B (or any other server)
  3. Validation should succeed

If validation fails with properly entered captcha codes, check your Data Protection configuration.

When to use Shared Key Stateless Captcha:

  • ✅ Full control over encryption keys
  • ✅ Works without ASP.NET Core Data Protection API
  • ✅ Simpler cluster configuration
  • ✅ Custom key rotation strategies
  • ✅ Works across different application frameworks
  • ✅ No dependency on external storage for keys

1. Register in DI with Shared Key

services.AddSharedKeyStatelessCaptcha(options =>
{
    options.SharedKey = "your-32-byte-base64-encoded-key"; // Generate securely
    options.FontStyle = FontStyle.Bold;
    options.DrawLines = true;
    options.TokenExpiration = TimeSpan.FromMinutes(5);
});

2. Generate Secure Shared Key

Important: Use a cryptographically secure random key. Here's how to generate one:

// Generate a secure 256-bit key (one-time setup)
using (var rng = RandomNumberGenerator.Create())
{
    var keyBytes = new byte[32]; // 256 bits
    rng.GetBytes(keyBytes);
    var base64Key = Convert.ToBase64String(keyBytes);
    Console.WriteLine($"Shared Key: {base64Key}");
}

3. Configuration Options

Configuration File (appsettings.json)

{
  "CaptchaSettings": {
    "SharedKey": "your-generated-base64-key-here",
    "TokenExpirationMinutes": 5
  }
}
public void ConfigureServices(IServiceCollection services)
{
    var captchaKey = Configuration["CaptchaSettings:SharedKey"];
    var expirationMinutes = Configuration.GetValue<int>("CaptchaSettings:TokenExpirationMinutes", 5);
    
    services.AddSharedKeyStatelessCaptcha(options =>
    {
        options.SharedKey = captchaKey;
        options.TokenExpiration = TimeSpan.FromMinutes(expirationMinutes);
        // Other options...
    });
}

4. Example Controller and View

See: src\Edi.Captcha.SampleApp\Controllers\SharedKeyStatelessController.cs and src\Edi.Captcha.SampleApp\Views\SharedKeyStateless\Index.cshtml for a complete example.

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

Packages
MoongladePure.Comments
Package Description

Version Last updated
5.1.1 10/4/2025
5.1.0 10/3/2025
5.0.1 10/3/2025
5.0.0 10/3/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