Edi.Captcha 5.1.1
Edi.Captcha.AspNetCore
The Captcha module used in my blog
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)
{
// ....
}
Stateless Captcha (Recommended for Scalable Applications)
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:
- Generate a captcha on Server A
- Submit the form to Server B (or any other server)
- Validation should succeed
If validation fails with properly entered captcha codes, check your Data Protection configuration.
Shared Key Stateless Captcha (Recommended for Scalable Applications without DPAPI)
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
|
.NET 8.0
- SixLabors.ImageSharp (>= 3.1.11)
- SixLabors.ImageSharp.Drawing (>= 2.1.7)
.NET 9.0
- SixLabors.ImageSharp (>= 3.1.11)
- SixLabors.ImageSharp.Drawing (>= 2.1.7)
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 |