Add support for more CAPTCHA providers
This commit is contained in:
parent
1ab7dd0626
commit
a76b4feecb
12 changed files with 302 additions and 6 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -398,3 +398,4 @@ FodyWeavers.xsd
|
||||||
*.sln.iml
|
*.sln.iml
|
||||||
.idea/
|
.idea/
|
||||||
config.toml
|
config.toml
|
||||||
|
CollabVMAuthServer/rockyou.txt
|
||||||
|
|
|
@ -5,4 +5,6 @@ public class LoginPayload
|
||||||
public string username { get; set; }
|
public string username { get; set; }
|
||||||
public string password { get; set; }
|
public string password { get; set; }
|
||||||
public string? captchaToken { get; set; }
|
public string? captchaToken { get; set; }
|
||||||
|
public string? turnstileToken { get; set; }
|
||||||
|
public string? recaptchaToken { get; set; }
|
||||||
}
|
}
|
|
@ -6,5 +6,7 @@ public class RegisterPayload
|
||||||
public string password { get; set; }
|
public string password { get; set; }
|
||||||
public string email { get; set; }
|
public string email { get; set; }
|
||||||
public string? captchaToken { get; set; }
|
public string? captchaToken { get; set; }
|
||||||
|
public string? turnstileToken { get; set; }
|
||||||
|
public string? recaptchaToken { get; set; }
|
||||||
public string dateOfBirth { get; set; }
|
public string dateOfBirth { get; set; }
|
||||||
}
|
}
|
|
@ -5,4 +5,6 @@ public class SendResetEmailPayload
|
||||||
public string email { get; set; }
|
public string email { get; set; }
|
||||||
public string username { get; set; }
|
public string username { get; set; }
|
||||||
public string? captchaToken { get; set; }
|
public string? captchaToken { get; set; }
|
||||||
|
public string? turnstileToken { get; set; }
|
||||||
|
public string? recaptchaToken { get; set; }
|
||||||
}
|
}
|
|
@ -4,6 +4,9 @@ public class AuthServerInformation
|
||||||
{
|
{
|
||||||
public bool registrationOpen { get; set; }
|
public bool registrationOpen { get; set; }
|
||||||
public AuthServerInformationCaptcha hcaptcha { get; set; }
|
public AuthServerInformationCaptcha hcaptcha { get; set; }
|
||||||
|
public AuthServerInformationTurnstile turnstile { get; set; }
|
||||||
|
public AuthServerInformationReCAPTCHA recaptcha { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthServerInformationCaptcha
|
public class AuthServerInformationCaptcha
|
||||||
|
@ -11,3 +14,15 @@ public class AuthServerInformationCaptcha
|
||||||
public bool required { get; set; }
|
public bool required { get; set; }
|
||||||
public string? siteKey { 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; }
|
||||||
|
}
|
|
@ -99,6 +99,55 @@ public static class Routes
|
||||||
}, Utilities.JsonSerializerOptions);
|
}, 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
|
// Check username and E-Mail
|
||||||
var user = await Program.Database.GetUser(payload.username);
|
var user = await Program.Database.GetUser(payload.username);
|
||||||
if (user == null || user.Email != payload.email)
|
if (user == null || user.Email != payload.email)
|
||||||
|
@ -716,6 +765,56 @@ public static class Routes
|
||||||
}, Utilities.JsonSerializerOptions);
|
}, 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
|
// Validate username and password
|
||||||
var user = await Program.Database.GetUser(payload.username);
|
var user = await Program.Database.GetUser(payload.username);
|
||||||
if (user == null || !Argon2.Verify(user.Password, payload.password))
|
if (user == null || !Argon2.Verify(user.Password, payload.password))
|
||||||
|
@ -936,6 +1035,56 @@ public static class Routes
|
||||||
}, Utilities.JsonSerializerOptions);
|
}, 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
|
// Make sure username isn't taken
|
||||||
var user = await Program.Database.GetUser(payload.username);
|
var user = await Program.Database.GetUser(payload.username);
|
||||||
if (user != null || await Program.Database.GetBot(payload.username) != null)
|
if (user != null || await Program.Database.GetBot(payload.username) != null)
|
||||||
|
@ -1074,7 +1223,21 @@ public static class Routes
|
||||||
new() {
|
new() {
|
||||||
required = Program.Config.hCaptcha.Enabled,
|
required = Program.Config.hCaptcha.Enabled,
|
||||||
siteKey = Program.Config.hCaptcha.Enabled ? Program.Config.hCaptcha.SiteKey : null
|
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
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,7 +9,8 @@ public class IConfig
|
||||||
public MySQLConfig MySQL { get; set; }
|
public MySQLConfig MySQL { get; set; }
|
||||||
public SMTPConfig SMTP { get; set; }
|
public SMTPConfig SMTP { get; set; }
|
||||||
public hCaptchaConfig hCaptcha { get; set; }
|
public hCaptchaConfig hCaptcha { get; set; }
|
||||||
|
public TurnstileConfig Turnstile { get; set; }
|
||||||
|
public ReCAPTCHAConfig ReCAPTCHA { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RegistrationConfig
|
public class RegistrationConfig
|
||||||
|
@ -66,3 +67,17 @@ public class hCaptchaConfig
|
||||||
public string? Secret { get; set; }
|
public string? Secret { get; set; }
|
||||||
public string? SiteKey { 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; }
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ public class Program
|
||||||
public static IConfig Config { get; private set; }
|
public static IConfig Config { get; private set; }
|
||||||
public static Database Database { get; private set; }
|
public static Database Database { get; private set; }
|
||||||
public static hCaptchaClient? hCaptcha { 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 Mailer? Mailer { get; private set; }
|
||||||
public static string[] BannedPasswords { get; set; }
|
public static string[] BannedPasswords { get; set; }
|
||||||
public static readonly Random Random = new Random();
|
public static readonly Random Random = new Random();
|
||||||
|
@ -75,6 +77,28 @@ public class Program
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.INFO, "hCaptcha disabled");
|
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
|
// load password list
|
||||||
BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt");
|
BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt");
|
||||||
// Configure web server
|
// Configure web server
|
||||||
|
|
30
CollabVMAuthServer/ReCAPTCHAClient.cs
Normal file
30
CollabVMAuthServer/ReCAPTCHAClient.cs
Normal 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; }
|
||||||
|
}
|
30
CollabVMAuthServer/TurnstileClient.cs
Normal file
30
CollabVMAuthServer/TurnstileClient.cs
Normal 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; }
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
|
|
|
@ -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.
|
# The subject and body of the E-Mail sent to users when they need to verify their E-Mail address.
|
||||||
VerificationCodeSubject = "CollabVM Account Verification"
|
VerificationCodeSubject = "CollabVM Account Verification"
|
||||||
VerificationCodeBody = """
|
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.
|
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.
|
# The subject and body of the E-Mail sent to users when they need to reset their password.
|
||||||
ResetPasswordSubject = "CollabVM Password Reset"
|
ResetPasswordSubject = "CollabVM Password Reset"
|
||||||
ResetPasswordBody = """
|
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.
|
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]
|
[hCaptcha]
|
||||||
# If true, hCaptcha will be used for the registration, login, and password reset forms.
|
# If true, hCaptcha will be used for the registration, login, and password reset forms.
|
||||||
Enabled = false
|
Enabled = false
|
||||||
|
@ -73,4 +75,16 @@ Enabled = false
|
||||||
Secret = ""
|
Secret = ""
|
||||||
SiteKey = ""
|
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 = ""
|
Loading…
Reference in a new issue