Add support for more CAPTCHA providers #1

Open
MDMCK10 wants to merge 1 commit from MDMCK10/CollabVMAuthServer:master into master
12 changed files with 302 additions and 6 deletions
Showing only changes of commit a76b4feecb - Show all commits

1
.gitignore vendored
View file

@ -398,3 +398,4 @@ FodyWeavers.xsd
*.sln.iml
.idea/
config.toml
CollabVMAuthServer/rockyou.txt

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -4,6 +4,9 @@ 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
@ -11,3 +14,15 @@ 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; }
}

View file

@ -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
},
});
}
}

View file

@ -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
@ -66,3 +67,17 @@ public class hCaptchaConfig
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; }
public string? SiteKey { get; set; }
}

View file

@ -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

View file

@ -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<ReCAPTCHAResponse> Verify(string token, string ip)
{
var response = await http.PostAsync("https://www.google.com/recaptcha/api/siteverify", new FormUrlEncodedContent(new []
{
new KeyValuePair<string, string>("secret", secret),
new KeyValuePair<string, string>("response", token),
new KeyValuePair<string, string>("remoteip", ip),
}));
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ReCAPTCHAResponse>() ?? 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; }
}

View file

@ -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<TurnstileResponse> Verify(string token, string ip)
{
var response = await http.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", new FormUrlEncodedContent(new []
{
new KeyValuePair<string, string>("secret", secret),
new KeyValuePair<string, string>("response", token),
new KeyValuePair<string, string>("remoteip", ip),
}));
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<TurnstileResponse>() ?? 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; }
}

View file

@ -1,6 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Computernewb.CollabVMAuthServer;

View file

@ -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 = ""