From a76b4feecb9f4d86e260a3d89767093d7a393eb1 Mon Sep 17 00:00:00 2001 From: MDMCK10 <21245760+MDMCK10@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:43:09 +0200 Subject: [PATCH] Add support for more CAPTCHA providers --- .gitignore | 1 + .../HTTP/Payloads/LoginPayload.cs | 2 + .../HTTP/Payloads/RegisterPayload.cs | 2 + .../HTTP/Payloads/SendResetEmailPayload.cs | 2 + .../HTTP/Responses/AuthServerInformation.cs | 15 ++ CollabVMAuthServer/HTTP/Routes.cs | 165 +++++++++++++++++- CollabVMAuthServer/IConfig.cs | 17 +- CollabVMAuthServer/Program.cs | 24 +++ CollabVMAuthServer/ReCAPTCHAClient.cs | 30 ++++ CollabVMAuthServer/TurnstileClient.cs | 30 ++++ CollabVMAuthServer/hCaptchaClient.cs | 2 - config.example.toml | 18 +- 12 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 CollabVMAuthServer/ReCAPTCHAClient.cs create mode 100644 CollabVMAuthServer/TurnstileClient.cs diff --git a/.gitignore b/.gitignore index dfec5a8..1ae9693 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,4 @@ FodyWeavers.xsd *.sln.iml .idea/ config.toml +CollabVMAuthServer/rockyou.txt diff --git a/CollabVMAuthServer/HTTP/Payloads/LoginPayload.cs b/CollabVMAuthServer/HTTP/Payloads/LoginPayload.cs index 7b66f02..da0e307 100644 --- a/CollabVMAuthServer/HTTP/Payloads/LoginPayload.cs +++ b/CollabVMAuthServer/HTTP/Payloads/LoginPayload.cs @@ -5,4 +5,6 @@ public class LoginPayload public string username { get; set; } public string password { get; set; } public string? captchaToken { get; set; } + public string? turnstileToken { get; set; } + public string? recaptchaToken { get; set; } } \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/RegisterPayload.cs b/CollabVMAuthServer/HTTP/Payloads/RegisterPayload.cs index 6666b03..fcc9db3 100644 --- a/CollabVMAuthServer/HTTP/Payloads/RegisterPayload.cs +++ b/CollabVMAuthServer/HTTP/Payloads/RegisterPayload.cs @@ -6,5 +6,7 @@ public class RegisterPayload public string password { get; set; } public string email { get; set; } public string? captchaToken { get; set; } + public string? turnstileToken { get; set; } + public string? recaptchaToken { get; set; } public string dateOfBirth { get; set; } } \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/SendResetEmailPayload.cs b/CollabVMAuthServer/HTTP/Payloads/SendResetEmailPayload.cs index d1dcf42..3d3edaf 100644 --- a/CollabVMAuthServer/HTTP/Payloads/SendResetEmailPayload.cs +++ b/CollabVMAuthServer/HTTP/Payloads/SendResetEmailPayload.cs @@ -5,4 +5,6 @@ public class SendResetEmailPayload public string email { get; set; } public string username { get; set; } public string? captchaToken { get; set; } + public string? turnstileToken { get; set; } + public string? recaptchaToken { get; set; } } \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/AuthServerInformation.cs b/CollabVMAuthServer/HTTP/Responses/AuthServerInformation.cs index 3f87823..a5ae7eb 100644 --- a/CollabVMAuthServer/HTTP/Responses/AuthServerInformation.cs +++ b/CollabVMAuthServer/HTTP/Responses/AuthServerInformation.cs @@ -4,9 +4,24 @@ public class AuthServerInformation { public bool registrationOpen { get; set; } public AuthServerInformationCaptcha hcaptcha { get; set; } + public AuthServerInformationTurnstile turnstile { get; set; } + public AuthServerInformationReCAPTCHA recaptcha { get; set; } + } public class AuthServerInformationCaptcha +{ + public bool required { get; set; } + public string? siteKey { get; set; } +} + +public class AuthServerInformationTurnstile +{ + public bool required { get; set; } + public string? siteKey { get; set; } +} + +public class AuthServerInformationReCAPTCHA { public bool required { get; set; } public string? siteKey { get; set; } diff --git a/CollabVMAuthServer/HTTP/Routes.cs b/CollabVMAuthServer/HTTP/Routes.cs index edc7b58..2c52444 100644 --- a/CollabVMAuthServer/HTTP/Routes.cs +++ b/CollabVMAuthServer/HTTP/Routes.cs @@ -99,6 +99,55 @@ public static class Routes }, Utilities.JsonSerializerOptions); } } + + // Check Turnstile response + if (Program.Config.Turnstile.Enabled) + { + if (string.IsNullOrWhiteSpace(payload.turnstileToken)) + { + context.Response.StatusCode = 400; + return Results.Json(new SendResetEmailResponse + { + success = false, + error = "Missing Turnstile token" + }, Utilities.JsonSerializerOptions); + } + var result = await Program.Turnstile!.Verify(payload.turnstileToken, ip.ToString()); + if (!result.success) + { + context.Response.StatusCode = 400; + return Results.Json(new SendResetEmailResponse + { + success = false, + error = "Invalid Turnstile response" + }, Utilities.JsonSerializerOptions); + } + } + + // Check reCAPTCHA response + if (Program.Config.ReCAPTCHA.Enabled) + { + if (string.IsNullOrWhiteSpace(payload.recaptchaToken)) + { + context.Response.StatusCode = 400; + return Results.Json(new SendResetEmailResponse + { + success = false, + error = "Missing reCAPTCHA token" + }, Utilities.JsonSerializerOptions); + } + var result = await Program.ReCAPTCHA!.Verify(payload.recaptchaToken, ip.ToString()); + if (!result.success) + { + context.Response.StatusCode = 400; + return Results.Json(new SendResetEmailResponse + { + success = false, + error = "Invalid reCAPTCHA response" + }, Utilities.JsonSerializerOptions); + } + } + // Check username and E-Mail var user = await Program.Database.GetUser(payload.username); if (user == null || user.Email != payload.email) @@ -716,6 +765,56 @@ public static class Routes }, Utilities.JsonSerializerOptions); } } + + + // Check Turnstile response + if (Program.Config.Turnstile.Enabled) + { + if (string.IsNullOrWhiteSpace(payload.turnstileToken)) + { + context.Response.StatusCode = 400; + return Results.Json(new LoginResponse + { + success = false, + error = "Missing Turnstile token" + }, Utilities.JsonSerializerOptions); + } + var result = await Program.Turnstile!.Verify(payload.turnstileToken, ip.ToString()); + if (!result.success) + { + context.Response.StatusCode = 400; + return Results.Json(new LoginResponse + { + success = false, + error = "Invalid Turnstile response" + }, Utilities.JsonSerializerOptions); + } + } + + // Check reCAPTCHA response + if (Program.Config.ReCAPTCHA.Enabled) + { + if (string.IsNullOrWhiteSpace(payload.recaptchaToken)) + { + context.Response.StatusCode = 400; + return Results.Json(new LoginResponse + { + success = false, + error = "Missing reCAPTCHA token" + }, Utilities.JsonSerializerOptions); + } + var result = await Program.ReCAPTCHA!.Verify(payload.recaptchaToken, ip.ToString()); + if (!result.success) + { + context.Response.StatusCode = 400; + return Results.Json(new LoginResponse + { + success = false, + error = "Invalid reCAPTCHA response" + }, Utilities.JsonSerializerOptions); + } + } + // Validate username and password var user = await Program.Database.GetUser(payload.username); if (user == null || !Argon2.Verify(user.Password, payload.password)) @@ -936,6 +1035,56 @@ public static class Routes }, Utilities.JsonSerializerOptions); } } + + + // Check Turnstile response + if (Program.Config.Turnstile.Enabled) + { + if (string.IsNullOrWhiteSpace(payload.turnstileToken)) + { + context.Response.StatusCode = 400; + return Results.Json(new RegisterResponse + { + success = false, + error = "Missing Turnstile token" + }, Utilities.JsonSerializerOptions); + } + var result = await Program.Turnstile!.Verify(payload.turnstileToken, ip.ToString()); + if (!result.success) + { + context.Response.StatusCode = 400; + return Results.Json(new RegisterResponse + { + success = false, + error = "Invalid Turnstile response" + }, Utilities.JsonSerializerOptions); + } + } + + // Check reCAPTCHA response + if (Program.Config.ReCAPTCHA.Enabled) + { + if (string.IsNullOrWhiteSpace(payload.recaptchaToken)) + { + context.Response.StatusCode = 400; + return Results.Json(new RegisterResponse + { + success = false, + error = "Missing reCAPTCHA token" + }, Utilities.JsonSerializerOptions); + } + var result = await Program.ReCAPTCHA!.Verify(payload.recaptchaToken, ip.ToString()); + if (!result.success) + { + context.Response.StatusCode = 400; + return Results.Json(new RegisterResponse + { + success = false, + error = "Invalid reCAPTCHA response" + }, Utilities.JsonSerializerOptions); + } + } + // Make sure username isn't taken var user = await Program.Database.GetUser(payload.username); if (user != null || await Program.Database.GetBot(payload.username) != null) @@ -1074,7 +1223,21 @@ public static class Routes new() { required = Program.Config.hCaptcha.Enabled, siteKey = Program.Config.hCaptcha.Enabled ? Program.Config.hCaptcha.SiteKey : null - } + }, + + turnstile = + new() + { + required = Program.Config.Turnstile.Enabled, + siteKey = Program.Config.Turnstile.Enabled ? Program.Config.Turnstile.SiteKey : null + }, + + recaptcha = + new() + { + required = Program.Config.ReCAPTCHA.Enabled, + siteKey = Program.Config.ReCAPTCHA.Enabled ? Program.Config.ReCAPTCHA.SiteKey : null + }, }); } } \ No newline at end of file diff --git a/CollabVMAuthServer/IConfig.cs b/CollabVMAuthServer/IConfig.cs index feb8f8c..3029789 100644 --- a/CollabVMAuthServer/IConfig.cs +++ b/CollabVMAuthServer/IConfig.cs @@ -9,7 +9,8 @@ public class IConfig public MySQLConfig MySQL { get; set; } public SMTPConfig SMTP { get; set; } public hCaptchaConfig hCaptcha { get; set; } - + public TurnstileConfig Turnstile { get; set; } + public ReCAPTCHAConfig ReCAPTCHA { get; set; } } public class RegistrationConfig @@ -61,6 +62,20 @@ public class SMTPConfig } public class hCaptchaConfig +{ + public bool Enabled { get; set; } + public string? Secret { get; set; } + public string? SiteKey { get; set; } +} + +public class TurnstileConfig +{ + public bool Enabled { get; set; } + public string? Secret { get; set; } + public string? SiteKey { get; set; } +} + +public class ReCAPTCHAConfig { public bool Enabled { get; set; } public string? Secret { get; set; } diff --git a/CollabVMAuthServer/Program.cs b/CollabVMAuthServer/Program.cs index f57f512..4fd91c3 100644 --- a/CollabVMAuthServer/Program.cs +++ b/CollabVMAuthServer/Program.cs @@ -10,6 +10,8 @@ public class Program public static IConfig Config { get; private set; } public static Database Database { get; private set; } public static hCaptchaClient? hCaptcha { get; private set; } + public static TurnstileClient? Turnstile { get; private set; } + public static ReCAPTCHAClient? ReCAPTCHA { get; private set; } public static Mailer? Mailer { get; private set; } public static string[] BannedPasswords { get; set; } public static readonly Random Random = new Random(); @@ -75,6 +77,28 @@ public class Program { Utilities.Log(LogLevel.INFO, "hCaptcha disabled"); } + + // Create Turnstile client + if (Config.Turnstile.Enabled) + { + Turnstile = new TurnstileClient(Config.Turnstile.Secret!); + Utilities.Log(LogLevel.INFO, "Turnstile enabled"); + } + else + { + Utilities.Log(LogLevel.INFO, "Turnstile disabled"); + } + + // Create reCAPTCHA client + if (Config.ReCAPTCHA.Enabled) + { + ReCAPTCHA = new ReCAPTCHAClient(Config.ReCAPTCHA.Secret!); + Utilities.Log(LogLevel.INFO, "reCAPTCHA enabled"); + } + else + { + Utilities.Log(LogLevel.INFO, "reCAPTCHA disabled"); + } // load password list BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt"); // Configure web server diff --git a/CollabVMAuthServer/ReCAPTCHAClient.cs b/CollabVMAuthServer/ReCAPTCHAClient.cs new file mode 100644 index 0000000..47d5b86 --- /dev/null +++ b/CollabVMAuthServer/ReCAPTCHAClient.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace Computernewb.CollabVMAuthServer; + +public class ReCAPTCHAClient(string secret) +{ + private string secret = secret; + private HttpClient http = new HttpClient(); + + public async Task Verify(string token, string ip) + { + var response = await http.PostAsync("https://www.google.com/recaptcha/api/siteverify", new FormUrlEncodedContent(new [] + { + new KeyValuePair("secret", secret), + new KeyValuePair("response", token), + new KeyValuePair("remoteip", ip), + })); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to parse reCAPTCHA response"); + } +} + +public class ReCAPTCHAResponse +{ + public bool success { get; set; } + public string challenge_ts { get; set; } + public string hostname { get; set; } + [JsonPropertyName("error-codes")] + public string[]? error_codes { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/TurnstileClient.cs b/CollabVMAuthServer/TurnstileClient.cs new file mode 100644 index 0000000..a5ae822 --- /dev/null +++ b/CollabVMAuthServer/TurnstileClient.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace Computernewb.CollabVMAuthServer; + +public class TurnstileClient(string secret) +{ + private string secret = secret; + private HttpClient http = new HttpClient(); + + public async Task Verify(string token, string ip) + { + var response = await http.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", new FormUrlEncodedContent(new [] + { + new KeyValuePair("secret", secret), + new KeyValuePair("response", token), + new KeyValuePair("remoteip", ip), + })); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to parse Turnstile response"); + } +} + +public class TurnstileResponse +{ + public bool success { get; set; } + public string challenge_ts { get; set; } + public string hostname { get; set; } + [JsonPropertyName("error-codes")] + public string[]? error_codes { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/hCaptchaClient.cs b/CollabVMAuthServer/hCaptchaClient.cs index 1efbb22..7387351 100644 --- a/CollabVMAuthServer/hCaptchaClient.cs +++ b/CollabVMAuthServer/hCaptchaClient.cs @@ -1,6 +1,4 @@ -using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; namespace Computernewb.CollabVMAuthServer; diff --git a/config.example.toml b/config.example.toml index e029e7b..6fb0776 100644 --- a/config.example.toml +++ b/config.example.toml @@ -54,18 +54,20 @@ FromEmail = "noreply@example.com" # The subject and body of the E-Mail sent to users when they need to verify their E-Mail address. VerificationCodeSubject = "CollabVM Account Verification" VerificationCodeBody = """ -Howdy! Someone (probably you) has tried to create a CollabVM account with this E-Mail. If this was you, your verification code is: $CODE +Hello! Someone (probably you) has tried to create a CollabVM account with this E-Mail. If this was you, your verification code is: $CODE If this was not you, someone probably entered your e-mail by mistake. If this is the case, you can safely ignore this e-mail. """ # The subject and body of the E-Mail sent to users when they need to reset their password. ResetPasswordSubject = "CollabVM Password Reset" ResetPasswordBody = """ -Howdy, $USERNAME! Someone (probably you) has sent a request to reset the password to your CollabVM account with this E-Mail. If this was you, your verification code is: $CODE +Hello, $USERNAME! Someone (probably you) has sent a request to reset the password to your CollabVM account with this E-Mail. If this was you, your verification code is: $CODE If this was not you, disregard this E-Mail and your password will remain unchanged. Do not share this code with anyone. """ +# Note: You can have multiple CAPTCHA providers enabled at the same time. + [hCaptcha] # If true, hCaptcha will be used for the registration, login, and password reset forms. Enabled = false @@ -73,4 +75,16 @@ Enabled = false Secret = "" SiteKey = "" +[Turnstile] +# If true, Turnstile will be used for the registration, login, and password reset forms. +Enabled = false +# The Turnstile site key and secret key. +Secret = "" +SiteKey = "" +[ReCAPTCHA] +# If true, reCAPTCHA will be used for the registration, login, and password reset forms. +Enabled = false +# The reCAPTCHA site key and secret key. +Secret = "" +SiteKey = "" \ No newline at end of file