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
<PackageReference Include="Persilsoft.Turnstile.Blazor" Version="1.0.5" />
<PackageVersion Include="Persilsoft.Turnstile.Blazor" Version="1.0.5" />
<PackageReference Include="Persilsoft.Turnstile.Blazor" />
paket add Persilsoft.Turnstile.Blazor --version 1.0.5
#r "nuget: Persilsoft.Turnstile.Blazor, 1.0.5"
#:package Persilsoft.Turnstile.Blazor@1.0.5
#addin nuget:?package=Persilsoft.Turnstile.Blazor&version=1.0.5
#tool nuget:?package=Persilsoft.Turnstile.Blazor&version=1.0.5
Persilsoft.Turnstile.Blazor
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.Factoryprovides 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 fromOnSuccesscallback
Returns: Result<bool, IEnumerable<string>> containing:
HasError-trueif verification failedSuccessValue-trueif verification succeededErrorValue- 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
OnSuccesscallback 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:
- Set
Appearance = Alwaysfor testing - Check browser developer tools for CSS issues
- 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" />
📦 Related Packages
Required
- Persilsoft.Captcha.Factory - Server-side multi-provider verification orchestration
- Persilsoft.Turnstile.Server - Cloudflare Turnstile server implementation
- Persilsoft.Result - Result pattern library (included as dependency)
Optional
- Persilsoft.Captcha.Contracts - Shared contracts for custom implementations
Alternative
- Persilsoft.Recaptcha.Blazor - Google reCAPTCHA v3 (invisible CAPTCHA)
🔗 Resources
- Cloudflare Turnstile Documentation
- Get Turnstile Keys
- Blazor Documentation
- Persilsoft.Result Documentation
⚖️ 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 | 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
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Persilsoft.Blazor.JSInterop (>= 1.0.14)
- Persilsoft.HttpDelegatingHandlers (>= 1.0.21)
- Persilsoft.Result (>= 1.0.6)
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.