Skip to content

Commit 10f8929

Browse files
committed
Better Turnstile response handling
1 parent 98faae2 commit 10f8929

File tree

4 files changed

+55
-11
lines changed

4 files changed

+55
-11
lines changed

API/Controller/Account/LoginV2.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ public async Task<IActionResult> LoginV2(
3838
var remoteIP = HttpContext.GetRemoteIP();
3939

4040
var turnStile = await turnstileService.VerifyUserResponseToken(body.TurnstileResponse, remoteIP, cancellationToken);
41-
if (!turnStile.IsT0) return Problem(TurnstileError.InvalidTurnstile);
41+
if (!turnStile.IsT0)
42+
{
43+
var cfErrors = turnStile.AsT1.Value!;
44+
if (cfErrors.All(err => err == CloduflareTurnstileError.InvalidResponse))
45+
return Problem(TurnstileError.InvalidTurnstile);
46+
47+
return Problem(new OpenShockProblem("InternalServerError", "Internal Server Error", HttpStatusCode.InternalServerError));
48+
}
4249

4350
var loginAction = await _accountService.Login(body.UsernameOrEmail, body.Password, new LoginContext
4451
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace OpenShock.Common.Services.Turnstile;
2+
3+
public enum CloduflareTurnstileError
4+
{
5+
MissingSecret,
6+
InvalidSecret,
7+
MissingResponse,
8+
InvalidResponse,
9+
BadRequest,
10+
TimeoutOrDuplicate,
11+
InternalServerError,
12+
}

Common/Services/Turnstile/CloudflareTurnstileService.cs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,31 @@ public CloudflareTurnstileService(HttpClient httpClient, IOptions<CloudflareTurn
2323
_logger = logger;
2424
}
2525

26+
private static Error<CloduflareTurnstileError[]> CreateError(params ReadOnlySpan<CloduflareTurnstileError> errors)
27+
{
28+
return new Error<CloduflareTurnstileError[]>(errors.ToArray());
29+
}
30+
31+
private static CloduflareTurnstileError MapCfError(string error)
32+
{
33+
return error switch
34+
{
35+
"missing-input-secret" => CloduflareTurnstileError.MissingSecret,
36+
"invalid-input-secret" => CloduflareTurnstileError.InvalidSecret,
37+
"missing-input-response" => CloduflareTurnstileError.MissingResponse,
38+
"invalid-input-response" => CloduflareTurnstileError.InvalidResponse,
39+
"bad-request" => CloduflareTurnstileError.BadRequest,
40+
"timeout-or-duplicate" => CloduflareTurnstileError.TimeoutOrDuplicate,
41+
"internal-error" => CloduflareTurnstileError.InternalServerError,
42+
_ => throw new ArgumentOutOfRangeException(nameof(error), error, null)
43+
};
44+
}
45+
2646
/// <inheritdoc />
27-
public async Task<OneOf<Success, MissingInput, Error, Error<IReadOnlyList<string>>>> VerifyUserResponseToken(
47+
public async Task<OneOf<Success, Error<CloduflareTurnstileError[]>>> VerifyUserResponseToken(
2848
string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default)
2949
{
30-
if (string.IsNullOrEmpty(responseToken)) return new MissingInput();
50+
if (string.IsNullOrEmpty(responseToken)) return CreateError(CloduflareTurnstileError.MissingResponse);
3151

3252
#if DEBUG
3353
if (responseToken == "dev-bypass") return new Success();
@@ -46,18 +66,23 @@ public async Task<OneOf<Success, MissingInput, Error, Error<IReadOnlyList<string
4666
using var httpResponse = await _httpClient.PostAsync(SiteVerifyEndpoint, httpContent, cancellationToken);
4767
if (!httpResponse.IsSuccessStatusCode)
4868
{
49-
_logger.LogWarning("Turnstile error: {StatusCode} {ReasonPhrase}", httpResponse.StatusCode,
50-
httpResponse.ReasonPhrase);
51-
return new Error();
69+
_logger.LogError("Turnstile error: {StatusCode} {ReasonPhrase}", httpResponse.StatusCode, httpResponse.ReasonPhrase);
70+
71+
return CreateError(httpResponse.StatusCode == HttpStatusCode.BadRequest ? CloduflareTurnstileError.BadRequest : CloduflareTurnstileError.InternalServerError);
5272
}
5373

54-
var response =
55-
await httpResponse.Content.ReadFromJsonAsync<CloudflareTurnstileVerifyResponseDto>(
56-
cancellationToken: cancellationToken);
74+
var response = await httpResponse.Content.ReadFromJsonAsync<CloudflareTurnstileVerifyResponseDto>(cancellationToken);
5775

5876
if (response.Success) return new Success();
77+
78+
var errors = response.ErrorCodes.Select(MapCfError).ToArray();
79+
80+
if (errors.All(err => err != CloduflareTurnstileError.InvalidResponse))
81+
{
82+
_logger.LogError("Turnstile error: {StatusCode} {ReasonPhrase}", httpResponse.StatusCode, string.Join(" ", errors.Select(err => err.ToString())));
83+
}
5984

60-
return new Error<IReadOnlyList<string>>(response.ErrorCodes);
85+
return CreateError(errors);
6186
}
6287
}
6388

Common/Services/Turnstile/ICloudflareTurnstileService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ public interface ICloudflareTurnstileService
1212
/// <param name="remoteIpAddress"></param>
1313
/// <param name="cancellationToken"></param>
1414
/// <returns>Success, No response token was supplied, internal error in cloudflare turnstile, business logic error on turnstile validation</returns>
15-
public Task<OneOf.OneOf<Success, MissingInput, Error, Error<IReadOnlyList<string>>>> VerifyUserResponseToken(
15+
public Task<OneOf.OneOf<Success, Error<CloduflareTurnstileError[]>>> VerifyUserResponseToken(
1616
string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default);
1717
}

0 commit comments

Comments
 (0)