Persilsoft.Turnstile.Blazor 1.0.5

dotnet add package Persilsoft.Turnstile.Blazor --version 1.0.5
                    
NuGet\Install-Package Persilsoft.Turnstile.Blazor -Version 1.0.5
                    
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.Turnstile.Blazor" Version="1.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Persilsoft.Turnstile.Blazor" Version="1.0.5" />
                    
Directory.Packages.props
<PackageReference Include="Persilsoft.Turnstile.Blazor" />
                    
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.Turnstile.Blazor --version 1.0.5
                    
#r "nuget: Persilsoft.Turnstile.Blazor, 1.0.5"
                    
#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.Turnstile.Blazor@1.0.5
                    
#: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.Turnstile.Blazor&version=1.0.5
                    
Install as a Cake Addin
#tool nuget:?package=Persilsoft.Turnstile.Blazor&version=1.0.5
                    
Install as a Cake Tool

Persilsoft.Turnstile.Blazor

NuGet License

A modern Blazor WebAssembly component library for Cloudflare Turnstile integration with clean architecture and built-in verification support using the Result pattern.


✨ Features

  • Cloudflare Turnstile Integration - User-friendly CAPTCHA alternative
  • Built-in Verification - Component handles both token generation and validation
  • Result Pattern - Returns Result<bool, IEnumerable<string>> for elegant error handling
  • Customizable UI - Theme, size, and appearance options
  • Clean Architecture - Well-structured, maintainable, testable code
  • Type-Safe - Full C# nullable reference types support
  • Detailed Error Codes - Specific error codes for different failure scenarios
  • Functional API - Support for both imperative and functional programming styles
  • Multiple Instances - Support for multiple components on the same page

📦 Installation

Client (Blazor WebAssembly)

dotnet add package Persilsoft.Turnstile.Blazor

Server (ASP.NET Core Web API)

dotnet add package Persilsoft.Captcha.Factory

Note: Persilsoft.Captcha.Factory provides the verification endpoint automatically.


⚙️ Configuration

Client Setup

1. Register services in Program.cs:

using Persilsoft.Turnstile.Blazor;
using Persilsoft.Turnstile.Blazor.Options;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddTurnstileServices(
    options => builder.Configuration
        .GetSection(TurnstileOptions.SectionKey)
        .Bind(options));

await builder.Build().RunAsync();

2. Configure appsettings.json:

{
  "TurnstileOptions": {
    "SiteKey": "your-turnstile-site-key",
    "WebApiBaseAddress": "https://your-api-domain.com"
  }
}

Server Setup

1. Register services and endpoint in Program.cs:

using Persilsoft.Captcha.Factory;

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddCaptcha(builder.Configuration);

var app = builder.Build();

// Register endpoint (POST /captcha/verify)
app.MapCaptchaVerifyEndpoint();

app.Run();

2. Configure appsettings.json:

{
  "CaptchaOptions": {
    "DefaultProvider": "turnstile",
    "Turnstile": {
      "SecretKey": "your-turnstile-secret-key",
      "VerifyEndpoint": "https://challenges.cloudflare.com/turnstile/v0/siteverify"
    }
  }
}

🚀 Usage

Basic Example (Imperative Style)

@page "/contact"
@using Persilsoft.Turnstile.Blazor.Components
@using Persilsoft.Turnstile.Blazor.Enums
@using Persilsoft.Result

<EditForm Model="Model" OnValidSubmit="HandleSubmitAsync">
    <DataAnnotationsValidator />
    
    <div class="mb-3">
        <label>Name</label>
        <InputText @bind-Value="Model.Name" class="form-control" />
        <ValidationMessage For="@(() => Model.Name)" />
    </div>

    <div class="mb-3">
        <label>Email</label>
        <InputText @bind-Value="Model.Email" class="form-control" />
        <ValidationMessage For="@(() => Model.Email)" />
    </div>

    
    <TurnstileComponent @ref="_turnstile"
                        Action="contact_form"
                        Theme="TurnstileTheme.Light"
                        Size="TurnstileSize.Normal"
                        OnSuccess="HandleSuccess"
                        OnError="HandleError"
                        OnExpired="HandleExpired" />

    @if (!string.IsNullOrEmpty(_errorMessage))
    {
        <div class="alert alert-danger">@_errorMessage</div>
    }

    <button type="submit" class="btn btn-primary" disabled="@_isSubmitting">
        @(_isSubmitting ? "Submitting..." : "Submit")
    </button>
</EditForm>

@code {
    private TurnstileComponent _turnstile = default!;
    private bool _isSubmitting;
    private string? _errorMessage;
    private string? _currentToken;
    private ContactModel Model { get; set; } = new();

    private async Task HandleSubmitAsync()
    {
        _isSubmitting = true;
        _errorMessage = null;
        
        try
        {
            // Token is automatically obtained when user completes challenge
            if (string.IsNullOrEmpty(_currentToken))
            {
                _errorMessage = "Please complete the security check";
                return;
            }

            // Verify token with Result pattern
            var result = await _turnstile.VerifyAsync(_currentToken);
            
            // Check for errors
            if (result.HasError)
            {
                var errors = result.ErrorValue!;
                _errorMessage = GetFriendlyErrorMessage(errors.FirstOrDefault());
                await _turnstile.ResetAsync(); // Reset widget for retry
                return;
            }

            // Process form
            await SubmitFormAsync();
        }
        finally
        {
            _isSubmitting = false;
        }
    }

    private void HandleSuccess(string token)
    {
        _currentToken = token;
        _errorMessage = null;
        StateHasChanged();
    }

    private void HandleError()
    {
        _errorMessage = "Security check failed. Please try again.";
        _currentToken = null;
        StateHasChanged();
    }

    private async Task HandleExpired()
    {
        _errorMessage = "Security check expired. Please try again.";
        _currentToken = null;
        await _turnstile.ResetAsync();
        StateHasChanged();
    }

    private string GetFriendlyErrorMessage(string? errorCode)
    {
        return errorCode switch
        {
            "missing-token" => "Security check not completed.",
            "invalid-token" => "Security check is invalid. Please try again.",
            "network-error" => "Network error. Please check your connection.",
            "timeout-error" => "Verification timed out. Please try again.",
            _ => "Security verification failed. Please try again."
        };
    }

    private async Task SubmitFormAsync()
    {
        // Your form submission logic
        await Task.Delay(1000);
        _errorMessage = "Form submitted successfully!";
    }

    public class ContactModel
    {
        [Required]
        public string Name { get; set; } = string.Empty;
        
        [Required, EmailAddress]
        public string Email { get; set; } = string.Empty;
    }
}

Functional Style with HandleError

@code {
    private async Task HandleSubmitAsync()
    {
        if (string.IsNullOrEmpty(_currentToken))
        {
            _errorMessage = "Please complete the security check";
            return;
        }

        _isSubmitting = true;
        
        try
        {
            var result = await _turnstile.VerifyAsync(_currentToken);
            
            // Functional approach with HandleError
            result.HandleError(
                onSuccess: async _ =>
                {
                    await SubmitFormAsync();
                    _errorMessage = "Success!";
                },
                onError: async errors =>
                {
                    var primaryError = errors.FirstOrDefault();
                    _errorMessage = GetFriendlyErrorMessage(primaryError);
                    await _turnstile.ResetAsync();
                }
            );
        }
        finally
        {
            _isSubmitting = false;
        }
    }
}

Login Example with Auto Theme

@page "/login"
@using Persilsoft.Turnstile.Blazor.Components
@using Persilsoft.Turnstile.Blazor.Enums

<EditForm Model="Model" OnValidSubmit="HandleLoginAsync">
    <InputText @bind-Value="Model.Email" placeholder="Email" class="form-control mb-3" />
    <InputText @bind-Value="Model.Password" type="password" placeholder="Password" class="form-control mb-3" />
    
    
    <TurnstileComponent @ref="_turnstile"
                        Action="login"
                        Theme="TurnstileTheme.Auto"
                        Size="TurnstileSize.Normal"
                        OnSuccess="token => _token = token"
                        OnError="HandleError"
                        OnExpired="HandleExpired" />
    
    @if (!string.IsNullOrEmpty(_errorMessage))
    {
        <div class="alert alert-danger">@_errorMessage</div>
    }
    
    <button type="submit" class="btn btn-primary w-100">Login</button>
</EditForm>

@code {
    private TurnstileComponent _turnstile = default!;
    private string? _token;
    private string? _errorMessage;
    private LoginModel Model { get; set; } = new();

    private async Task HandleLoginAsync()
    {
        if (string.IsNullOrEmpty(_token))
        {
            _errorMessage = "Please complete the security check";
            return;
        }

        var result = await _turnstile.VerifyAsync(_token);
        
        if (result.HasError)
        {
            _errorMessage = "Security verification failed";
            await _turnstile.ResetAsync();
            return;
        }
        
        await AuthService.LoginAsync(Model);
    }

    private void HandleError() => _errorMessage = "Security check failed";
    
    private async Task HandleExpired()
    {
        _errorMessage = "Security check expired";
        await _turnstile.ResetAsync();
    }
}

Compact Widget Example


<TurnstileComponent Action="newsletter_signup"
                    Size="TurnstileSize.Compact"
                    Theme="TurnstileTheme.Light"
                    OnSuccess="token => _newsletterToken = token"
                    OnError="() => ShowError('Verification failed')"
                    OnExpired="() => ShowError('Verification expired')" />

📚 API Reference

TurnstileComponent

Parameters
Parameter Type Default Required Description
Action string - ✅ Yes Action identifier for analytics (e.g., "login", "register", "contact")
Theme TurnstileTheme? Auto No Widget theme: Light, Dark, or Auto
Size TurnstileSize Normal No Widget size: Normal, Compact
Appearance TurnstileAppearance Always No Widget visibility: Always, Execute, InteractionOnly
Language string? Browser default No Language code (e.g., "es", "en", "fr")
OnSuccess EventCallback<string> - ✅ Yes Callback when user successfully completes challenge (receives token)
OnError EventCallback - ✅ Yes Callback when challenge encounters an error
OnExpired EventCallback - ✅ Yes Callback when token expires
Methods
VerifyAsync(string token)

Verifies the token with your backend using the Result pattern.

Task<Result<bool, IEnumerable<string>>> VerifyAsync(string token)

Parameters:

  • token - Token received from OnSuccess callback

Returns: Result<bool, IEnumerable<string>> containing:

  • HasError - true if verification failed
  • SuccessValue - true if verification succeeded
  • ErrorValue - Collection of error codes if verification failed

Example:

var result = await _turnstile.VerifyAsync(token);

if (result.HasError)
{
    var errors = result.ErrorValue!;
    Console.WriteLine($"Errors: {string.Join(", ", errors)}");
}
ResetAsync()

Resets the widget to allow user to retry.

ValueTask ResetAsync()

Example:

if (result.HasError)
{
    await _turnstile.ResetAsync(); // Reset for retry
}
RemoveAsync()

Removes the widget from the DOM.

ValueTask RemoveAsync()

Example:

// Clean up when component is no longer needed
await _turnstile.RemoveAsync();

🎨 Enums

TurnstileTheme

Widget color theme.

public enum TurnstileTheme
{
    Light,  // Light theme
    Dark,   // Dark theme
    Auto    // Follows system preference (default)
}

TurnstileSize

Widget size.

public enum TurnstileSize
{
    Normal,   // Standard size: 300x65px (default)
    Compact   // Compact size: 130x120px
}

TurnstileAppearance

Widget visibility behavior.

public enum TurnstileAppearance
{
    Always,           // Always visible (default)
    Execute,          // Hidden until explicitly executed
    InteractionOnly   // Only shown when interaction is required
}

🔍 Error Codes

The VerifyAsync() method returns detailed error codes in Result.ErrorValue:

Error Code Description Recommended Action
missing-token Token is null or empty Ensure user completed challenge
invalid-token Token is invalid or already used Reset widget and retry
network-error Network communication error Retry automatically or ask user to check connection
timeout-error Verification request timed out Retry with exponential backoff
configuration-error Backend configuration issue Check server configuration
http-error-XXX HTTP error from Cloudflare (e.g., http-error-502) Retry or show service unavailable message

Example of handling specific errors:

var result = await _turnstile.VerifyAsync(token);

if (result.HasError)
{
    var primaryError = result.ErrorValue!.FirstOrDefault();
    
    var (message, shouldReset) = primaryError switch
    {
        "invalid-token" => 
            ("Verification expired. Please try again.", true),
        "network-error" or "timeout-error" => 
            ("Connection error. Retrying...", false),
        _ => 
            ("Verification failed. Please try again.", true)
    };
    
    ShowError(message);
    
    if (shouldReset)
        await _turnstile.ResetAsync();
}

🔧 How It Works

┌─────────────────────────────────────────────────────────────┐
│              Blazor WebAssembly (Client)                    │
├─────────────────────────────────────────────────────────────┤
│  1. User completes Turnstile challenge                      │
│  2. OnSuccess(token) → Store token                          │
│  3. VerifyAsync(token) → POST /captcha/verify               │
│  4. Receive Result<bool, IEnumerable<string>>               │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              ASP.NET Core API (Server)                      │
├─────────────────────────────────────────────────────────────┤
│  5. Validate token with Cloudflare                          │
│  6. Return Result<bool, IEnumerable<string>> to client      │
└─────────────────────────────────────────────────────────────┘

🐛 Troubleshooting

Token Not Being Generated

Cause: User hasn't completed the challenge.
Solution:

  • Ensure OnSuccess callback is properly wired
  • Check browser console for JavaScript errors
  • Verify Site Key in configuration

Verification Returns invalid-token

Common Causes:

  • Token already used (tokens are single-use)
  • Token expired (valid for ~5 minutes)
  • Mismatched keys (Site Key vs Secret Key)

Solution:

if (result.ErrorValue?.Contains("invalid-token") == true)
{
    await _turnstile.ResetAsync(); // Allow user to retry
}

Widget Not Visible

Causes:

  • Appearance = InteractionOnly (intended behavior)
  • CSS conflicts
  • Ad blocker interference

Solution:

  1. Set Appearance = Always for testing
  2. Check browser developer tools for CSS issues
  3. Temporarily disable ad blockers

network-error or timeout-error

Solution: Implement retry logic:

private async Task<Result<bool, IEnumerable<string>>> VerifyWithRetryAsync(
    string token, 
    int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        var result = await _turnstile.VerifyAsync(token);

        if (!result.HasError)
            return result;

        var error = result.ErrorValue!.FirstOrDefault();
        if (error is not ("network-error" or "timeout-error") || attempt == maxRetries)
            return result;

        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)));
    }

    return new Result<bool, IEnumerable<string>>(new[] { "max-retries-exceeded" });
}

CORS Issues

Configure CORS in your backend:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowClient", policy =>
        policy.WithOrigins("https://your-domain.com")
              .AllowAnyMethod()
              .AllowAnyHeader());
});

app.UseCors("AllowClient");

🎯 Best Practices

1. Always Handle All Three Callbacks

<TurnstileComponent OnSuccess="HandleSuccess"
                    OnError="HandleError"
                    OnExpired="HandleExpired" />

2. Reset on Errors

private async Task HandleError()
{
    _errorMessage = "Verification failed";
    await _turnstile.ResetAsync(); // Allow retry
}

3. Store Token Immediately

private void HandleSuccess(string token)
{
    _currentToken = token;
    _errorMessage = null;
    // Optionally auto-submit form
    // await HandleSubmitAsync();
}

4. Use Appropriate Size


<TurnstileComponent Size="TurnstileSize.Normal" />


<TurnstileComponent Size="TurnstileSize.Compact" />

5. Match Theme to Your Design


<TurnstileComponent Theme="TurnstileTheme.Light" />


<TurnstileComponent Theme="TurnstileTheme.Dark" />


<TurnstileComponent Theme="TurnstileTheme.Auto" />

6. Use Specific Action Names


<TurnstileComponent Action="login" />
<TurnstileComponent Action="register" />
<TurnstileComponent Action="checkout" />

Required

Optional

Alternative


🔗 Resources


⚖️ Turnstile vs reCAPTCHA

Feature Turnstile reCAPTCHA v3
Visibility Visible widget Invisible
User Interaction May require interaction No interaction
Privacy Privacy-focused, no tracking Tracks user behavior
Speed Fast challenges Background analysis
Customization Theme, size options Limited customization
Best For Privacy-conscious apps Invisible security

📄 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.


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 (1)

Showing the top 1 NuGet packages that depend on Persilsoft.Turnstile.Blazor:

Package Downloads
Persilsoft.Membership.Blazor

Contains razor clases for use in frontend membership projects

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.5 171 12/31/2025
1.0.4 104 12/31/2025
1.0.3 712 12/2/2025
1.0.2 686 12/2/2025
1.0.1 1,269 5/31/2025
1.0.0 219 5/28/2025