Compare commits

...

9 commits

Author SHA1 Message Date
nexy7574
b71186d958
chore(recaptcha): Update example config file
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 3s
Release Docker Image / define-variables (push) Failing after 1s
Checks / Rust / Format (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Clippy (push) Failing after 37s
Checks / Rust / Cargo Test (push) Failing after 35s
Unsure how this managed to get past the `git commit -S -a`
but sure
2025-07-08 19:47:42 +01:00
nexy7574
c362499cef
docs(recaptcha): Clarify registration when token & captcha are configured 2025-07-08 19:34:47 +01:00
nexy7574
14774fa153
feat(recaptcha): Don't allow pubkey without associated privkey 2025-07-08 19:32:23 +01:00
nexy7574
ff805d8ae1
feat(recaptcha): Fix linting issues after the linter fix lied to me 2025-07-08 19:27:51 +01:00
nexy7574
f0994355d4
feat(recaptcha): Fix linting issues 2025-07-08 19:15:55 +01:00
nexy7574
980774a275
feat(recaptcha): Update example config after previous changes 2025-07-08 19:08:25 +01:00
nexy7574
e4a6abe15e
feat(recaptcha): Disable treating captcha-enabled servers as abuse-prone 2025-07-08 19:07:27 +01:00
nexy7574
df1cb10a8e
feat(recaptcha): Add documentation for new fields 2025-07-08 19:03:35 +01:00
nexy7574
651d07a609
feat: Add ReCaptcha registration flow 2025-07-08 18:58:05 +01:00
7 changed files with 123 additions and 20 deletions

12
Cargo.lock generated
View file

@ -1076,6 +1076,7 @@ dependencies = [
"loole",
"lru-cache",
"rand 0.8.5",
"recaptcha-verify",
"regex",
"reqwest",
"ruma",
@ -3751,6 +3752,17 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "recaptcha-verify"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e3be7b2e46e24637ac96b0c9f70070f188652018573f36f4e511dcad09738a"
dependencies = [
"reqwest",
"serde",
"serde_json",
]
[[package]]
name = "redox_syscall"
version = "0.5.13"

View file

@ -441,6 +441,26 @@
#
#registration_token_file =
# The public site key for reCaptcha. If this is provided, reCaptcha
# becomes required during registration. If both captcha *and*
# registration token are enabled, both will be required during
# registration.
#
# IMPORTANT: "Verify the origin of reCAPTCHA solutions" **MUST** BE
# DISABLED IF YOU WANT THE CAPTCHA TO WORK IN 3RD PARTY CLIENTS, OR
# CLIENTS HOSTED ON DOMAINS OTHER THAN YOUR OWN!
#
# Registration must be enabled (`allow_registration` must be true) for
# this to have any effect.
#
#recaptcha_site_key =
# The private site key for reCaptcha.
# If this is omitted, captcha registration will not work,
# even if `recaptcha_site_key` is set.
#
#recaptcha_private_site_key =
# Controls whether encrypted rooms and events are allowed.
#
#allow_encryption = true

View file

@ -291,19 +291,34 @@ pub(crate) async fn register_route(
}
// UIAA
let mut uiaainfo;
let skip_auth = if services.globals.registration_token.is_some() {
let mut uiaainfo = UiaaInfo {
flows: Vec::new(),
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
let mut skip_auth = body.appservice_info.is_some();
if services.globals.registration_token.is_some() {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some()
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
});
}
if services.config.recaptcha_private_site_key.is_some() {
if let Some(pubkey) = &services.config.recaptcha_site_key {
// ReCaptcha required
uiaainfo
.flows
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
"m.login.recaptcha": {
"public_key": pubkey,
},
}))
.expect("Failed to serialize recaptcha params");
skip_auth = skip_auth || is_guest;
}
} else {
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
@ -313,8 +328,8 @@ pub(crate) async fn register_route(
session: None,
auth_error: None,
};
body.appservice_info.is_some() || is_guest
};
skip_auth = skip_auth || is_guest;
}
if !skip_auth {
match &body.auth {

View file

@ -180,19 +180,28 @@ pub fn check(config: &Config) -> Result {
}
}
if config.recaptcha_site_key.is_some() && config.recaptcha_private_site_key.is_none() {
return Err!(Config(
"recaptcha_private_site_key",
"reCAPTCHA private site key is required when reCAPTCHA site key is set."
));
}
if config.allow_registration
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
&& config.recaptcha_site_key.is_none()
{
return Err!(Config(
"registration_token",
"!! You have `allow_registration` enabled without a token configured in your config \
which means you are allowing ANYONE to register on your conduwuit instance without \
any 2nd-step (e.g. registration token). If this is not the intended behaviour, \
please set a registration token. For security and safety reasons, conduwuit will \
shut down. If you are extra sure this is the desired behaviour you want, please \
set the following config option to true:
"!! You have `allow_registration` enabled without a token or captcha configured \
which means you are allowing ANYONE to register on your continuwuity instance \
without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \
abused by malicious actors. If this is not the intended behaviour, please set a \
registration token. For security and safety reasons, continuwuity will shut down. \
If you are extra sure this is the desired behaviour you want, please set the \
following config option to true:
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
));
}

View file

@ -556,6 +556,24 @@ pub struct Config {
/// example: "/etc/continuwuity/.reg_token"
pub registration_token_file: Option<PathBuf>,
/// The public site key for reCaptcha. If this is provided, reCaptcha
/// becomes required during registration. If both captcha *and*
/// registration token are enabled, both will be required during
/// registration.
///
/// IMPORTANT: "Verify the origin of reCAPTCHA solutions" **MUST** BE
/// DISABLED IF YOU WANT THE CAPTCHA TO WORK IN 3RD PARTY CLIENTS, OR
/// CLIENTS HOSTED ON DOMAINS OTHER THAN YOUR OWN!
///
/// Registration must be enabled (`allow_registration` must be true) for
/// this to have any effect.
pub recaptcha_site_key: Option<String>,
/// The private site key for reCaptcha.
/// If this is omitted, captcha registration will not work,
/// even if `recaptcha_site_key` is set.
pub recaptcha_private_site_key: Option<String>,
/// Controls whether encrypted rooms and events are allowed.
#[serde(default = "true_fn")]
pub allow_encryption: bool,

View file

@ -111,6 +111,7 @@ webpage.workspace = true
webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false }
[lints]
workspace = true

View file

@ -177,6 +177,34 @@ pub async fn try_auth(
// Password was correct! Let's add it to `completed`
uiaainfo.completed.push(AuthType::Password);
},
| AuthData::ReCaptcha(r) => {
if self.services.config.recaptcha_private_site_key.is_none() {
return Err!(Request(Forbidden("ReCaptcha is not configured.")));
}
match recaptcha_verify::verify(
self.services
.config
.recaptcha_private_site_key
.as_ref()
.unwrap(),
r.response.as_str(),
None,
)
.await
{
| Ok(()) => {
uiaainfo.completed.push(AuthType::ReCaptcha);
},
| Err(e) => {
error!("ReCaptcha verification failed: {e:?}");
uiaainfo.auth_error = Some(ruma::api::client::error::StandardErrorBody {
kind: ErrorKind::forbidden(),
message: "ReCaptcha verification failed.".to_owned(),
});
return Ok((false, uiaainfo));
},
}
},
| AuthData::RegistrationToken(t) => {
let tokens = self.read_tokens().await?;
if tokens.contains(t.token.trim()) {