Ce guide vaut pour ASP.NET Core 8 et plus récent, avec les minimal APIs ; dans un contrôleur MVC, l'appel est le même. Un seul HttpClient sert le processus et garde la connexion ouverte, et un CancellationTokenSource borne l'appel entier, connexion comprise.
Le tag
Placez le tag dans le <head> du layout qu'utilisent vos pages d'inscription, de connexion et de checkout (Pages/Shared/_Layout.cshtml ou Views/Shared/_Layout.cshtml) :
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
L'appel
// Korzen.cs using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; public sealed class Korzen(string apiKey) { // The headers Bot Protection reads: those one browser build sends alike, and Accept-Language. static readonly string[] Forwarded = ["accept", "accept-encoding", "accept-language", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"]; // One for the process: it keeps the connection open, and opens a new one every 15 minutes to follow DNS. static readonly HttpClient Http = new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(15) }); /// <summary>The answer for this request, or null on any error: then go on as on allow (fail open).</summary> public async Task<JsonElement?> Decide(HttpContext context, string path) { var request = context.Request; var headers = new Dictionary<string, string>(); foreach (var name in Forwarded) { if (request.Headers.TryGetValue(name, out var value)) { headers[name] = value.ToString(); } } var ip = context.Connection.RemoteIpAddress; // see "The visitor's IP" using var deadline = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); // the whole call try { using var call = new HttpRequestMessage(HttpMethod.Post, "https://korzen.dev/v1/decide") { Content = JsonContent.Create(new Dictionary<string, object?> { ["ip"] = ip is { IsIPv4MappedToIPv6: true } ? ip.MapToIPv4().ToString() : ip?.ToString(), ["user_agent"] = request.Headers.UserAgent.ToString(), ["headers"] = headers, ["token"] = request.Cookies["_kz"], ["path"] = path, ["method"] = "POST", }), }; call.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); call.Headers.Add("Korzen-Deadline-Ms", "200"); using var res = await Http.SendAsync(call, deadline.Token); if (res.StatusCode != HttpStatusCode.OK) { return null; } return await res.Content.ReadFromJsonAsync<JsonElement>(deadline.Token); } catch (Exception) { return null; // a timeout, a network error or an answer that is no JSON } } }
Kestrel ne garde aucun ordre des headers de la requête, donc aucun n'est envoyé. Puis, dans Program.cs, les forwarded headers et l'endpoint qui crée le compte :
// Program.cs using System.Net; using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(new Korzen(builder.Configuration["KORZEN_API_KEY"] ?? "")); builder.Services.Configure<ForwardedHeadersOptions>(o => { o.ForwardedHeaders = ForwardedHeaders.XForwardedFor; // your proxy's entry: see "The visitor's IP" // o.KnownProxies.Add(IPAddress.Parse("10.0.0.5")); // your proxy, when it is not on this machine }); var app = builder.Build(); app.UseForwardedHeaders(); app.MapPost("/signup", async (HttpContext context, Korzen korzen) => { var d = await korzen.Decide(context, "/signup"); if (d?.GetProperty("action").GetString() == "block") { return Results.Text("We could not create your account.", statusCode: 400); } // "challenge": your CAPTCHA, or an email or phone check, before going on var user = await CreateAccount(await context.Request.ReadFormAsync()); // your own code await SaveRequestId(user, d?.GetProperty("request_id").GetString()); // see "Keep the request_id" return Results.Redirect("/welcome"); }); app.Run();
L'IP du visiteur
Connection.RemoteIpAddress est l'adresse de la connexion, celle de votre proxy si vous en avez un. UseForwardedHeaders avec XForwardedFor la remplace par la dernière entrée de X-Forwarded-For, celle que votre proxy a ajoutée, mais seulement quand la connexion vient d'un proxy connu : le loopback par défaut. Ajoutez l'adresse de votre load balancer à KnownProxies (ou son réseau aux réseaux connus), et appelez UseForwardedHeaders avant tout ce qui lit l'IP. Derrière Cloudflare, lisez plutôt context.Request.Headers["CF-Connecting-IP"], si votre serveur ne reçoit du trafic que de Cloudflare.
Appliquer l'action
Ne lisez que action. Avec block, répondez avec la même erreur que l'action affiche quand elle échoue pour toute autre raison, pour qu'un bot n'apprenne rien. Avec challenge, utilisez la vérification que vous avez déjà (un CAPTCHA, un code par e-mail ou téléphone) ; sans aucune, continuez. Le compte commence en mode monitor, où action est toujours allow : rien ne change pour les visiteurs tant que vous n'activez pas le mode block.
Garder le request_id
Chaque réponse a un request_id. Gardez-le avec ce que l'action a créé, dans une colonne du compte ou de la commande, ou dans une ligne de log avec leur identifiant. Il désigne cette décision quand vous nous interrogez à son sujet, et vous permet de retrouver plus tard quelle réponse a laissé entrer un faux compte.
Tester sur votre machine
Lancez dotnet run et ouvrez-le sur localhost. Les pages sur localhost acceptent toujours le tag et n'apparaissent jamais comme domaine. En local, l'IP est 127.0.0.1 ou ::1, et c'est très bien.
- Définissez
KORZEN_API_KEYdans l'environnement (ou avecdotnet user-secrets set KORZEN_API_KEY …) et ouvrez la page d'inscription. Dans les outils de développement du navigateur, le cookie_kzapparaît après quelques secondes. - Inscrivez-vous. La décision apparaît sur la page Décisions du tableau de bord, sous le chemin que vous avez envoyé.
- Vérifiez le fail-open : mettez une mauvaise clé et inscrivez-vous à nouveau. L'appel répond
401, et l'inscription passe.
Erreurs fréquentes
- Un load balancer absent de
KnownProxies. Le middleware ignore alorsX-Forwarded-For, et chaque visiteur a l'IP privée du load balancer, que Bot Protection traite comme une absence d'IP. - Vider
KnownProxieset les réseaux connus pour faire confiance à chaque saut. L'IP devient ce que le visiteur a écrit dansX-Forwarded-For. new HttpClient()pour chaque appel. Chacun ouvre sa propre connexion, avec une poignée de main TLS dans votre timeout, et peut épuiser les sockets.- Appeler
/v1/decidedepuis le navigateur. L'API key atteindrait chaque visiteur ; l'appel a sa place sur votre serveur. - Pas de timeout, ou une erreur qui bloque l'inscription. Toute réponse autre que
200, ou l'absence de réponse à temps, signifie : laissez passer le visiteur.