Persiltech.Turnstile.Blazor 1.0.3

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

Persiltech.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 Persiltech.Turnstile.Blazor

Server (ASP.NET Core Web API)

dotnet add package Persiltech.Captcha.Factory

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


⚙️ Configuration

Client Setup

1. Register services in Program.cs:

using Persiltech.Turnstile.Blazor;
using Persiltech.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 Persiltech.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 Persiltech.Turnstile.Blazor.Components
@using Persiltech.Turnstile.Blazor.Enums
@using Persiltech.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 Persiltech.Turnstile.Blazor.Components
@using Persiltech.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 © 2026 Persiltech. 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 Persiltech.Turnstile.Blazor:

Package Downloads
Persiltech.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.4 110 7/5/2026
1.0.3 100 6/28/2026
1.0.2 114 6/6/2026
1.0.1 103 5/5/2026
1.0.0 108 5/3/2026