Skip to content

Commit

Permalink
update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
bchavez committed Sep 4, 2023
1 parent 923c063 commit 058e295
Showing 1 changed file with 33 additions and 144 deletions.
177 changes: 33 additions & 144 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ BitArmory.Turnstile for .NET and C#

Project Description
-------------------
:recycle: A minimal, no-drama, friction-less **C#** **HTTP** verification client for **Cloudflare**'s [**Turnstile** API](https://developers.cloudflare.com/turnstile/).
A minimal, no-drama, friction-less **C#** **HTTP** verification client for **Cloudflare**'s [**Turnstile** API](https://developers.cloudflare.com/turnstile/).

The problem with current **Turnstile** libraries in **.NET** is that all of them take a hard dependency on the underlying web framework like **ASP.NET WebForms**, **ASP.NET MVC 5**, **ASP.NET Core**, or **ASP.NET Razor Pages**.

Expand Down Expand Up @@ -39,7 +39,7 @@ Install-Package BitArmory.Turnstile
General Usage
-------------
### Getting Started
You'll need a **Cloudflare** account. You can sign up [here](https://www.cloudflare.com/)! After you sign up and setup your domain, you'll have two important pieces of information:
You'll need a **Cloudflare** account. Get started by following the directions [here](https://developers.cloudflare.com/turnstile/get-started/)! After you sign up and you're signed into the dashboard; you'll need two important pieces of information:
1. Your `site` key
2. Your `secret` key

Expand All @@ -52,202 +52,91 @@ This library supports all widget types:
## Turnstile
### Client-side Setup

Be sure to checkout [this video that describes how Turnstile v3 works](https://www.youtube.com/watch?v=tbvxFW4UJdU) before implementing.
Be sure to [read the documentation](https://developers.cloudflare.com/turnstile/) before implementing.

Then, on every page of your website, add the following JavaScript:
Then, to protect an **HTML** form submission on your website, add the following:
```html
<html>
<head>
<script src='https://www.google.com/Turnstile/api.js?render=GOOGLE_SITE_KEY'></script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
...
<script>
gTurnstile.ready(function() {
gTurnstile.execute('GOOGLE_SITE_KEY', {action: 'TAG'});
});
</script>
</body>
</html>
```
Every page should call `gTurnstile.execute` with some unique **action** `TAG`. [Read more about actions in the official docs here](https://developers.google.com/Turnstile/docs/v3#actions).

When it is time to validate an **HTTP** `POST` you'll need transfer the captcha `token` in the browser to a hidden HTML form field as shown below:

```html
<html>
<body>
<form action='/do-post' method='POST'>
<input id="captcha" type="hidden" name="captcha" value="" />
</form>
<script>
function ExecuteTurnstile_OnSome_ButtonAction(){
gTurnstile.ready(function() {
gTurnstile.execute('GOOGLE_SITE_KEY', {action: 'SomeAction'})
.then(function(token) {
// Set `token` in a hidden form input.
$("#captcha").val(token);
//And finally submit the form by firing
//off the HTTP POST over the wire at this
//exact moment in time here.
});
});
}
</script>
</body>
</html>
```
You'll need to execute `ExecuteTurnstile_OnSome_ButtonAction()` function the moment the user decides to submit your form. Otherwise, if you run `gTurnstile.*` code during page load, the token being copied to the hidden field can expire after a few minutes. This means, if the user takes a long time filling out a form, the token copied at page load can expire and your server will validate an expired token by the time the form is submitted resulting in a failed captcha verification.

Therefore, you should execute the `ExecuteTurnstile_OnSome_ButtonAction()` function on some `onclick=` event to get a fresh token before the form is submitted.

Also, keep in mind, `gTurnstile.execute()` returns a **JavaScript Promise**. You won't have a valid token in your `<form>` until the line `$("#captcha").val(token);` above executes. So you'll need to postpone the form submission until `$("#captcha").val(token);` is actually executed. Then, *and only then,* you can continue submitting the HTML form to have it validated on your server with a valid token.

### Verifying the POST Server-side
When the `POST` is received on the server:
1. Get the client's IP address. If you're using **CloudFlare**, be sure to use the [`CF-Connecting-IP` header value][0].
2. Extract the `#captcha` value (client token) in the hidden **HTML** form field.
3. Use the `TurnstileService` to verify the client's **Turnstile** is valid.

```csharp
//1. Get the client IP address in your chosen web framework
string clientIp = GetClientIpAddress();
string token = null;
string secret = "your_secret_key";

//2. Extract the `#captcha` field from the hidden HTML form in your chosen web framework
if( this.Request.Form.TryGetValue("captcha", out var formField) )
{
token = formField;
}

//3. Validate the Turnstile with Google
var captchaApi = new TurnstileService();
var result = await captchaApi.Verify3Async(token, clientIp, secret);

if( !result.IsSuccess || result.Action != "SOME_ACTION" || result.Score < 0.5 )
{
// The POST is not valid
return new BadRequestResult();
}
else{
//continue processing, everything is okay!
}
```

<details><summary>GetClientIpAddress() in ASP.NET Core</summary>
<p>

**Note:** If your site is behind CloudFlare, be sure you're suing the [`CF-Connecting-IP` header value][0] instead.

```csharp
public string GetClientIpAddress(){
return this.HttpContext.Connection.RemoteIpAddress.ToString();
}
```

</p>
</details>

<details><summary>GetClientIpAddress() in ASP.NET WebForms</summary>
<p>

**Note:** If your site is behind CloudFlare, be sure you're suing the [`CF-Connecting-IP` header value][0] instead.

```csharp
public string GetClientIpAddress(){
return this.Request.UserHostAddress;
}
```

</p>
</details>

You'll want to make sure the action name you choose for the request is legitimate. The `result.Score` is the probably of a human. So, you'll want to make sure you have a `result.Score > 0.5`; anything less is probably a bot.

## Turnstile v2 (I'm not a robot)
### Client-side Setup
Add the following `<div class="g-Turnstile">` and `<script>` tags to your **HTML** form:
```html
<html>
<body>
<form method="POST">
<form method='POST' action='/my-form-post-endpoint'>
...
<div class="g-Turnstile" data-sitekey="your_site_key"></div>
<div class="cf-turnstile" data-sitekey="your_site_key"></div>
<input type="submit" value="Submit">
</form>

<script src="https://www.google.com/Turnstile/api.js" async defer></script>
</body>
</html>
```

When the user visits the **HTML** form, a hidden form field `cf-turnstile-response` will be added to the **HTML** form above. The `cf-turnstile-response` represents a token of the captcha challenge result and will need to be verified server-side when the **HTML** form is posted to your server.

### Verifying the POST Server-side
When the `POST` is received on the server:
1. Get the client's IP address. If you're using **CloudFlare**, be sure to use the [`CF-Connecting-IP` header value][0].
2. Extract the `g-Turnstile-response` (Client Response) **HTML** form field.
3. Use the `TurnstileService` to verify the client's **Turnstile** is valid.
When the **HTML** form `POST` is received on the server:
1. Get the client's IP address. If you're using **Cloudflare**, be sure to use the [`CF-Connecting-IP` header value][0].
2. Extract the hidden form field `cf-turnstile-response` from the browser's form POST submission.
3. Use the `TurnstileService` to verify that the client's **Turnstile** `cf-turnstile-response` challenge is valid.

The following example shows how to verify the captcha during an **HTTP** `POST` back in **ASP.NET Core: Razor Pages**.
The following example shows how to verify the captcha during an **HTTP** form `POST` back in **ASP.NET Core: Razor Pages**.

```csharp
//1. Get the client IP address in your chosen web framework
string clientIp = GetClientIpAddress();
string captchaResponse = null;
string secret = "your_secret_key";
string clientIp = GetClientIpAddress();
string browserChallengeToken = null;

//2. Extract the `g-Turnstile-response` field from the HTML form in your chosen web framework
if( this.Request.Form.TryGetValue(Constants.ClientResponseKey, out var formField) )
//2. Extract the `cf-turnstile-response` hidden field from the HTML form in your chosen web framework
//Tip: You can also use Constants.ClientResponseFormKey instead of the magic string below
if( this.Request.Form.TryGetValue("cf-turnstile-response", out var hiddenFormField) )
{
capthcaResponse = formField;
browserChallengeToken = hiddenFormField;
}

//3. Validate the Turnstile with Google
var captchaApi = new TurnstileService();
var isValid = await captchaApi.Verify2Async(capthcaResponse, clientIp, secret);
if( !isValid )
//3. Validate the Turnstile challenge with Cloudflare
var turnstileApi = new TurnstileService();
var verifyResult = await turnstileApi.VerifyAsync(browserChallengeToken, secret, clientIp);
if( verifyResult.IsSuccess is false )
{
this.ModelState.AddModelError("captcha", "The Turnstile is not valid.");
this.ModelState.AddModelError("captcha", "The Cloudflare challenge is not valid.");
return new BadRequestResult();
}
else{
//continue processing, everything is okay!
}
```

<details><summary>GetClientIpAddress() in ASP.NET Core</summary>
**Notes:**
* The `TurnstileService.VerifyAsync` supports an optional idempotency parameter; you can read more about that [here](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#accepted-parameters).
* The `clientIp` is technically optional but providing the client's IP address prevents abuses by ensuring that the current HTTP request is the one that received the token.

<details><summary><b>GetClientIpAddress() in ASP.NET Core</b></summary>
<p>

**Note:** If your site is behind CloudFlare, be sure you're suing the [`CF-Connecting-IP` header value][0] instead.
**Note:** If your site is behind Cloudflare, be sure you're using the [`CF-Connecting-IP` header value][0] instead.

```csharp
public string GetClientIpAddress(){
return this.HttpContext.Connection.RemoteIpAddress.ToString();
}
```

</p>
</details>

<details><summary>GetClientIpAddress() in ASP.NET WebForms</summary>
<details><summary><b>GetClientIpAddress() in ASP.NET WebForms</b></summary>
<p>

**Note:** If your site is behind CloudFlare, be sure you're suing the [`CF-Connecting-IP` header value][0] instead.
**Note:** If your site is behind Cloudflare, be sure you're using the [`CF-Connecting-IP` header value][0] instead.

```csharp
public string GetClientIpAddress(){
return this.Request.UserHostAddress;
}
```

</p>
</details>
</details>

That's it! **Happy verifying!** :tada:


Building
--------
* Download the source code.
Expand Down

0 comments on commit 058e295

Please sign in to comment.