Persilsoft.Captcha.Factory
1.0.1
dotnet add package Persilsoft.Captcha.Factory --version 1.0.1
NuGet\Install-Package Persilsoft.Captcha.Factory -Version 1.0.1
<PackageReference Include="Persilsoft.Captcha.Factory" Version="1.0.1" />
<PackageVersion Include="Persilsoft.Captcha.Factory" Version="1.0.1" />
<PackageReference Include="Persilsoft.Captcha.Factory" />
paket add Persilsoft.Captcha.Factory --version 1.0.1
#r "nuget: Persilsoft.Captcha.Factory, 1.0.1"
#:package Persilsoft.Captcha.Factory@1.0.1
#addin nuget:?package=Persilsoft.Captcha.Factory&version=1.0.1
#tool nuget:?package=Persilsoft.Captcha.Factory&version=1.0.1
Persilsoft.Captcha.Factory
Backend orchestration layer for multiple captcha providers (Google reCAPTCHA v3, Cloudflare Turnstile) with automatic endpoint registration and clean architecture implementation using the Result pattern.
โจ Features
- Multi-Provider Support - Google reCAPTCHA v3 and Cloudflare Turnstile
- Automatic Endpoint Registration - Single line setup with
MapCaptchaVerifyEndpoint() - Result Pattern Integration - Returns
Result<bool, IEnumerable<string>>for elegant error handling - Clean Architecture - Thin interactor, service abstraction, dependency injection
- Factory Pattern - Easy provider switching via configuration
- Comprehensive Error Codes - Detailed error information for different failure scenarios
- OpenAPI/Swagger Ready - Full metadata for API documentation
- Minimal API - Modern ASP.NET Core endpoint routing
- Zero Additional Dependencies - Works directly with JSON strings
๐ฆ Installation
dotnet add package Persilsoft.Captcha.Factory
This package automatically includes:
Persilsoft.Recaptcha.Server- Google reCAPTCHA v3 implementationPersilsoft.Turnstile.Server- Cloudflare Turnstile implementationPersilsoft.Result- Result pattern library
๐ Quick Start
1. Install Package
dotnet add package Persilsoft.Captcha.Factory
2. Configure Services
Program.cs:
using Persilsoft.Captcha.Factory;
var builder = WebApplication.CreateBuilder(args);
// Register captcha services
builder.Services.AddCaptcha(builder.Configuration);
var app = builder.Build();
// Register captcha verification endpoint
app.MapCaptchaVerifyEndpoint();
app.Run();
3. Configure Provider
appsettings.json:
{
"CaptchaOptions": {
"DefaultProvider": "recaptcha",
"Recaptcha": {
"SecretKey": "your-recaptcha-secret-key",
"VerifyEndpoint": "https://www.google.com/recaptcha/api/siteverify",
"MinimumScore": 0.5
}
}
}
That's it! Your API now has a fully functional captcha verification endpoint at POST /captcha/verify.
๐๏ธ Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Client (Blazor/JavaScript) โ
โ โข Persilsoft.Recaptcha.Blazor โ
โ โข Persilsoft.Turnstile.Blazor โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP POST
โ Body: "token-string"
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Persilsoft.Captcha.Factory (Backend) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Endpoints.cs (Infrastructure Layer) โ
โ โข POST /captcha/verify โ
โ โข Accepts JSON string token โ
โ โข Maps Result โ HTTP status codes โ
โ โผ โ
โ VerifyCaptchaInteractor (Application Layer) โ
โ โข Thin orchestrator (30 lines) โ
โ โข Delegates to ICaptchaService โ
โ โข Adds optional logging โ
โ โผ โ
โ ICaptchaService (Abstraction) โ
โ โข Returns Result<bool, IEnumerable<string>> โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโดโโโโโโโโโโโโ
โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ ReCaptchaService โ โ TurnstileService โ
โ (Google v3) โ โ (Cloudflare) โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ๏ธ Configuration
ReCaptcha v3
appsettings.json:
{
"CaptchaOptions": {
"DefaultProvider": "recaptcha",
"Recaptcha": {
"SecretKey": "6LdXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"VerifyEndpoint": "https://www.google.com/recaptcha/api/siteverify",
"MinimumScore": 0.5
}
}
}
Configuration Options:
| Option | Required | Description | Default |
|--------|----------|-------------|---------|
| SecretKey | โ
Yes | Your reCAPTCHA secret key | - |
| VerifyEndpoint | No | Google verification API endpoint | https://www.google.com/recaptcha/api/siteverify |
| MinimumScore | No | Minimum acceptable score (0.0-1.0) | 0.5 |
Get Keys: Google reCAPTCHA Admin Console
Cloudflare Turnstile
appsettings.json:
{
"CaptchaOptions": {
"DefaultProvider": "turnstile",
"Turnstile": {
"SecretKey": "0x4XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"VerifyEndpoint": "https://challenges.cloudflare.com/turnstile/v0/siteverify"
}
}
}
Configuration Options:
| Option | Required | Description | Default |
|--------|----------|-------------|---------|
| SecretKey | โ
Yes | Your Turnstile secret key | - |
| VerifyEndpoint | No | Cloudflare verification API endpoint | https://challenges.cloudflare.com/turnstile/v0/siteverify |
Get Keys: Cloudflare Turnstile Dashboard
๐ก API Endpoint
POST /captcha/verify
Verifies a captcha token and returns a Result<bool, IEnumerable<string>>.
Request:
- Content-Type:
application/json - Body: Token string directly (no wrapper object)
"03AGdBq26PxXXXXXXXXXXXXXXXXXXXXXXXX"
Success Response (200 OK):
{
"hasError": false,
"successValue": true,
"errorValue": null
}
Error Response (400 Bad Request):
{
"hasError": true,
"successValue": null,
"errorValue": ["low-score:0.3", "invalid-token"]
}
HTTP Status Codes:
| Status | Description | Example Errors |
|--------|-------------|----------------|
| 200 | Verification successful | - |
| 400 | Client error | missing-token, invalid-token, low-score:0.3 |
| 500 | Server configuration error | configuration-error |
| 502 | Provider service error | network-error, timeout-error, http-error-502 |
๐ Error Codes
The endpoint returns detailed error codes in Result.ErrorValue:
| Error Code | Description | HTTP Status | Recommended Action |
|---|---|---|---|
missing-token |
Token is null or empty | 400 | Client must send token |
invalid-token |
Token is invalid or expired | 400 | Generate new token |
low-score:X.XX |
reCAPTCHA score below threshold | 400 | Additional verification or block |
network-error |
Network communication error | 502 | Retry with backoff |
timeout-error |
Request timed out | 502 | Retry with backoff |
configuration-error |
Backend misconfiguration | 500 | Check server config |
http-error-XXX |
HTTP error from provider | 502 | Retry or show maintenance message |
๐ป Usage Examples
Client Side (Blazor)
Using Persilsoft.Recaptcha.Blazor:
// Gateway sends token as string directly
public async Task<Result<bool, IEnumerable<string>>> VerifyAsync(string token)
{
try
{
// โ
Send token as JSON string directly
var response = await _http.PostAsJsonAsync("captcha/verify", token);
// Deserialize Result directly
var result = await response.Content
.ReadFromJsonAsync<Result<bool, IEnumerable<string>>>();
return result ?? new(new[] { "null-response" });
}
catch (HttpRequestException)
{
return new(new[] { "network-error" });
}
}
Client Side (JavaScript/Fetch)
async function verifyCaptcha(token) {
try {
const response = await fetch('/captcha/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(token) // Send token string directly
});
const result = await response.json();
if (result.hasError) {
console.error('Verification failed:', result.errorValue);
return false;
}
return result.successValue;
} catch (error) {
console.error('Network error:', error);
return false;
}
}
Server Side (Basic)
Minimal API Example:
using Persilsoft.Captcha.Factory;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddCaptcha(builder.Configuration);
var app = builder.Build();
// Map endpoint (automatically handles everything)
app.MapCaptchaVerifyEndpoint();
app.Run();
Server Side (With CORS)
var builder = WebApplication.CreateBuilder(args);
// Add CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorClient", policy =>
policy.WithOrigins("https://your-blazor-app.com")
.AllowAnyMethod()
.AllowAnyHeader());
});
builder.Services.AddCaptcha(builder.Configuration);
var app = builder.Build();
app.UseCors("AllowBlazorClient");
app.MapCaptchaVerifyEndpoint();
app.Run();
Server Side (With Custom Route)
// Instead of MapCaptchaVerifyEndpoint(), manually map:
app.MapPost("api/security/verify-captcha", async (
[FromBody] string token,
VerifyCaptchaInteractor interactor) =>
{
var result = await interactor.ExecuteAsync(token);
return result.HasError
? Results.BadRequest(result)
: Results.Ok(result);
});
Server Side (With Authentication)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(/* ... */);
builder.Services.AddAuthorization();
builder.Services.AddCaptcha(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Endpoint respects authorization
app.MapCaptchaVerifyEndpoint()
.RequireAuthorization(); // Optional: require authentication
app.Run();
Server Side (With Rate Limiting)
using Microsoft.AspNetCore.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("captcha", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 10;
});
});
app.UseRateLimiter();
app.MapCaptchaVerifyEndpoint()
.RequireRateLimiting("captcha");
Switching Providers
Change provider by updating appsettings.json:
{
"CaptchaOptions": {
"DefaultProvider": "turnstile", // Changed from "recaptcha"
"Recaptcha": {
"SecretKey": "..."
},
"Turnstile": {
"SecretKey": "..." // Use this one
}
}
}
No code changes required! ๐
๐งช Testing
Unit Testing Example
using Persilsoft.Captcha.Factory;
using Persilsoft.Result;
using Xunit;
using Moq;
public class CaptchaVerificationTests
{
[Fact]
public async Task VerifyAsync_ValidToken_ReturnsSuccess()
{
// Arrange
var mockService = new Mock<ICaptchaService>();
mockService
.Setup(s => s.VerifyTokenAsync(It.IsAny<string>(), default))
.ReturnsAsync(new Result<bool, IEnumerable<string>>(true));
var interactor = new VerifyCaptchaInteractor(
mockService.Object,
Mock.Of<ILogger<VerifyCaptchaInteractor>>());
// Act
var result = await interactor.ExecuteAsync("valid-token");
// Assert
Assert.False(result.HasError);
Assert.True(result.SuccessValue);
}
[Fact]
public async Task VerifyAsync_InvalidToken_ReturnsError()
{
// Arrange
var mockService = new Mock<ICaptchaService>();
mockService
.Setup(s => s.VerifyTokenAsync(It.IsAny<string>(), default))
.ReturnsAsync(new Result<bool, IEnumerable<string>>(
new[] { "invalid-token" }));
var interactor = new VerifyCaptchaInteractor(
mockService.Object,
Mock.Of<ILogger<VerifyCaptchaInteractor>>());
// Act
var result = await interactor.ExecuteAsync("invalid-token");
// Assert
Assert.True(result.HasError);
Assert.Contains("invalid-token", result.ErrorValue!);
}
[Fact]
public async Task VerifyAsync_NullToken_ThrowsArgumentException()
{
// Arrange
var mockService = new Mock<ICaptchaService>();
var interactor = new VerifyCaptchaInteractor(
mockService.Object,
Mock.Of<ILogger<VerifyCaptchaInteractor>>());
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => interactor.ExecuteAsync(null!));
}
}
Integration Testing
using Microsoft.AspNetCore.Mvc.Testing;
public class CaptchaEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public CaptchaEndpointTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task VerifyEndpoint_ValidRequest_Returns200()
{
// Arrange
var token = "test-token";
// Act
var response = await _client.PostAsJsonAsync("/captcha/verify", token);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content
.ReadFromJsonAsync<Result<bool, IEnumerable<string>>>();
Assert.NotNull(result);
}
[Fact]
public async Task VerifyEndpoint_EmptyToken_Returns400()
{
// Act
var response = await _client.PostAsJsonAsync("/captcha/verify", "");
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var result = await response.Content
.ReadFromJsonAsync<Result<bool, IEnumerable<string>>>();
Assert.True(result!.HasError);
Assert.Contains("missing-token", result.ErrorValue!);
}
}
๐ Troubleshooting
"Configuration section 'CaptchaOptions' not found"
Cause: Missing or incorrectly named configuration section.
Solution:
{
"CaptchaOptions": { // โ
Exact name required
"DefaultProvider": "recaptcha",
"Recaptcha": {
"SecretKey": "..."
}
}
}
"Unsupported or not configured captcha provider"
Causes:
DefaultProvideris empty or null- Provider name is misspelled
- Provider configuration is missing
Solution:
{
"CaptchaOptions": {
"DefaultProvider": "recaptcha", // โ
Must be "recaptcha" or "turnstile"
"Recaptcha": { // โ
Must have config for selected provider
"SecretKey": "your-key"
}
}
}
Verification Always Fails
Common Causes:
- Mismatched keys - Verify Site Key (client) matches Secret Key (server) pair
- Wrong domain - Domain in Google/Cloudflare console must match your domain
- Network issues - Server cannot reach Google/Cloudflare APIs
- Token expired - Token is older than 2 minutes (reCAPTCHA) or 5 minutes (Turnstile)
Debugging:
{
"Logging": {
"LogLevel": {
"Persilsoft.Captcha": "Debug" // Enable debug logging
}
}
}
Check logs for detailed error information:
[Debug] Verifying captcha token using ReCaptchaService
[Warning] Captcha verification failed with errors: invalid-token
502 Bad Gateway Errors
Causes:
- Network connectivity issues from server to provider
- Provider API is down
- Firewall blocking outbound requests
Solution:
# Test server connectivity to providers
curl https://www.google.com/recaptcha/api/siteverify
curl https://challenges.cloudflare.com/turnstile/v0/siteverify
If curl fails, check:
- Firewall rules
- Network configuration
- Proxy settings
CORS Errors in Browser
Cause: Backend not configured to accept requests from frontend domain.
Solution:
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowClient", policy =>
policy.WithOrigins("https://your-domain.com", "http://localhost:5000")
.AllowAnyMethod()
.AllowAnyHeader());
});
app.UseCors("AllowClient");
๐ฏ Best Practices
1. Use Environment Variables for Secrets
appsettings.json:
{
"CaptchaOptions": {
"Recaptcha": {
"SecretKey": "${RECAPTCHA_SECRET_KEY}"
}
}
}
Or with User Secrets in development:
dotnet user-secrets set "CaptchaOptions:Recaptcha:SecretKey" "your-secret-key"
2. Configure Different Providers per Environment
appsettings.Development.json:
{
"CaptchaOptions": {
"DefaultProvider": "recaptcha",
"Recaptcha": {
"MinimumScore": 0.3 // Lower threshold for development
}
}
}
appsettings.Production.json:
{
"CaptchaOptions": {
"DefaultProvider": "turnstile",
"Turnstile": {
"SecretKey": "${TURNSTILE_SECRET_KEY}"
}
}
}
3. Monitor Verification Rates
// VerifyCaptchaInteractor already logs:
// - Success/failure rates
// - Error codes
// - Service type used
// Check logs to monitor:
[Information] Captcha token verified successfully
[Warning] Captcha verification failed with errors: low-score:0.3
// Use Application Insights, Serilog, or similar for aggregation
4. Implement Rate Limiting
Prevent abuse by limiting verification requests:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("captcha", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 10; // 10 requests per minute per IP
});
});
app.MapCaptchaVerifyEndpoint()
.RequireRateLimiting("captcha");
5. Handle Provider Failover
For critical applications, implement fallback:
{
"CaptchaOptions": {
"DefaultProvider": "recaptcha",
"FallbackProvider": "turnstile", // Not implemented yet, but a good idea
"Recaptcha": { "SecretKey": "..." },
"Turnstile": { "SecretKey": "..." }
}
}
6. Validate on Backend Always
Never trust client-side validation alone:
// โ
ALWAYS verify on backend
var result = await interactor.ExecuteAsync(token);
if (result.HasError)
{
// Block the action
return Unauthorized();
}
// โ
Proceed with protected action
await ProcessFormAsync();
๐ฆ Related Packages
Frontend (Required for Full-Stack)
- Persilsoft.Recaptcha.Blazor - Blazor component for Google reCAPTCHA v3
- Persilsoft.Turnstile.Blazor - Blazor component for Cloudflare Turnstile
Backend Components (Included)
- Persilsoft.Recaptcha.Server - Google reCAPTCHA v3 server implementation
- Persilsoft.Turnstile.Server - Cloudflare Turnstile server implementation
Core Libraries (Included)
- Persilsoft.Result - Result pattern library
๐ Resources
- Google reCAPTCHA v3 Documentation
- Cloudflare Turnstile Documentation
- ASP.NET Core Minimal APIs
- Clean Architecture
๐ License
Copyright ยฉ 2025 Persilsoft. All rights reserved.
Licensed under the MIT License.
๐ง Support
For issues or questions, please open an issue on the GitHub repository.
๐ค Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
Made with โค๏ธ by Edinson Aldaz
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Persilsoft.Recaptcha.Server (>= 1.0.34)
- Persilsoft.Turnstile.Server (>= 1.0.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v1.0.0:
- Initial release
- Multiple captcha provider support (Factory pattern)
- ReCaptcha v3 integration
- Cloudflare Turnstile integration
- Automatic endpoint registration with Minimal APIs
- Comprehensive error handling and validation
- Clean architecture implementation
- Full XML documentation