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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Persilsoft.Captcha.Factory" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Persilsoft.Captcha.Factory" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Persilsoft.Captcha.Factory" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Persilsoft.Captcha.Factory --version 1.0.1
                    
#r "nuget: Persilsoft.Captcha.Factory, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Persilsoft.Captcha.Factory@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Persilsoft.Captcha.Factory&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Persilsoft.Captcha.Factory&version=1.0.1
                    
Install as a Cake Tool

Persilsoft.Captcha.Factory

NuGet License

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 implementation
  • Persilsoft.Turnstile.Server - Cloudflare Turnstile implementation
  • Persilsoft.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:

  1. DefaultProvider is empty or null
  2. Provider name is misspelled
  3. 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:

  1. Mismatched keys - Verify Site Key (client) matches Secret Key (server) pair
  2. Wrong domain - Domain in Google/Cloudflare console must match your domain
  3. Network issues - Server cannot reach Google/Cloudflare APIs
  4. 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();

Frontend (Required for Full-Stack)

Backend Components (Included)

Core Libraries (Included)


๐Ÿ”— Resources


๐Ÿ“„ 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:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Submit a pull request

Made with โค๏ธ by Edinson Aldaz

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 151 12/13/2025
1.0.0 127 12/13/2025

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