From 651d07a609bab68d70510d0c1eecddab1fcdf670 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 18:58:05 +0100 Subject: [PATCH 1/9] feat: Add ReCaptcha registration flow --- Cargo.lock | 12 ++++++++++++ conduwuit-example.toml | 8 ++++++++ src/api/client/account.rs | 40 ++++++++++++++++++++++++++------------- src/core/config/mod.rs | 4 ++++ src/service/Cargo.toml | 1 + src/service/uiaa/mod.rs | 28 +++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d950e9da..6f711007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/conduwuit-example.toml b/conduwuit-example.toml index b7456237..8358f7c7 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -441,6 +441,14 @@ # #registration_token_file = +# This item is undocumented. Please contribute documentation for it. +# +#recaptcha_site_key = + +# This item is undocumented. Please contribute documentation for it. +# +#recaptcha_private_site_key = + # Controls whether encrypted rooms and events are allowed. # #allow_encryption = true diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 12801e7d..624aa932 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -291,19 +291,33 @@ 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 = false; + 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], + }); + skip_auth = body.appservice_info.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 = body.appservice_info.is_some() || skip_auth; } else { // No registration token necessary, but clients must still go through the flow uiaainfo = UiaaInfo { @@ -313,7 +327,7 @@ pub(crate) async fn register_route( session: None, auth_error: None, }; - body.appservice_info.is_some() || is_guest + skip_auth = skip_auth || body.appservice_info.is_some() || is_guest; }; if !skip_auth { diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 282caf79..2307583c 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -556,6 +556,10 @@ pub struct Config { /// example: "/etc/continuwuity/.reg_token" pub registration_token_file: Option, + pub recaptcha_site_key: Option, + + pub recaptcha_private_site_key: Option, + /// Controls whether encrypted rooms and events are allowed. #[serde(default = "true_fn")] pub allow_encryption: bool, diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 8b0d1405..fdebd1d7 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -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 diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index 7803c736..e71889be 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -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()) { From df1cb10a8eb5908683ee1b5ce04491bc8037c991 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:03:35 +0100 Subject: [PATCH 2/9] feat(recaptcha): Add documentation for new fields --- src/api/client/account.rs | 31 ++++++++++++++++--------------- src/core/config/mod.rs | 13 +++++++++++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 624aa932..35e3435c 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -298,26 +298,27 @@ pub(crate) async fn register_route( session: None, auth_error: None, }; - let mut skip_auth = false; + let mut skip_auth = body.appservice_info.is_some(); if services.globals.registration_token.is_some() { // Registration token required uiaainfo.flows.push(AuthFlow { stages: vec![AuthType::RegistrationToken], }); - skip_auth = body.appservice_info.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 = body.appservice_info.is_some() || skip_auth; + if !services.config.recaptcha_private_site_key.is_none() { + 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 { @@ -327,7 +328,7 @@ pub(crate) async fn register_route( session: None, auth_error: None, }; - skip_auth = skip_auth || body.appservice_info.is_some() || is_guest; + skip_auth = skip_auth || is_guest; }; if !skip_auth { diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 2307583c..9df01d46 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -556,8 +556,21 @@ pub struct Config { /// example: "/etc/continuwuity/.reg_token" pub registration_token_file: Option, + /// The public site key for reCaptcha. If this is provided, reCaptcha + /// becomes required during registration, **even if token registration is + /// enabled**. + /// + /// 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, + /// 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, /// Controls whether encrypted rooms and events are allowed. From e4a6abe15e3236f99f8b5ee2866f6db212c71367 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:07:27 +0100 Subject: [PATCH 3/9] feat(recaptcha): Disable treating captcha-enabled servers as abuse-prone --- src/core/config/check.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/config/check.rs b/src/core/config/check.rs index 3dc45e2f..8a3d1f77 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -184,15 +184,17 @@ pub fn check(config: &Config) -> Result { && !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_some() && config.recaptcha_private_site_key.is_some()) { 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`" )); } From 980774a27595ca5d87a1fbd4cfecf285da763823 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:08:25 +0100 Subject: [PATCH 4/9] feat(recaptcha): Update example config after previous changes --- conduwuit-example.toml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 8358f7c7..c6301af5 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -441,11 +441,22 @@ # #registration_token_file = -# This item is undocumented. Please contribute documentation for it. +# The public site key for reCaptcha. If this is provided, reCaptcha +# becomes required during registration, **even if token registration is +# enabled**. +# +# 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 = -# This item is undocumented. Please contribute documentation for it. +# 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 = From f0994355d494a1acae34c9a899fd9ee27a9d2fb1 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:15:55 +0100 Subject: [PATCH 5/9] feat(recaptcha): Fix linting issues --- src/api/client/account.rs | 4 ++-- src/service/uiaa/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 35e3435c..11414abf 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -305,7 +305,7 @@ pub(crate) async fn register_route( stages: vec![AuthType::RegistrationToken], }); } - if !services.config.recaptcha_private_site_key.is_none() { + if services.config.recaptcha_private_site_key.is_some() { if let Some(pubkey) = &services.config.recaptcha_site_key { // ReCaptcha required uiaainfo @@ -329,7 +329,7 @@ pub(crate) async fn register_route( auth_error: None, }; skip_auth = skip_auth || is_guest; - }; + } if !skip_auth { match &body.auth { diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index e71889be..de08f227 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -192,7 +192,7 @@ pub async fn try_auth( ) .await { - | Ok(_) => { + | Ok() => { uiaainfo.completed.push(AuthType::ReCaptcha); }, | Err(e) => { From ff805d8ae16fbf2bc165dc3c05e37c8472ede7fc Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:27:51 +0100 Subject: [PATCH 6/9] feat(recaptcha): Fix linting issues after the linter fix lied to me --- src/service/uiaa/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index de08f227..7735c87f 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -192,7 +192,7 @@ pub async fn try_auth( ) .await { - | Ok() => { + | Ok(()) => { uiaainfo.completed.push(AuthType::ReCaptcha); }, | Err(e) => { From 14774fa15358911cd88e1e848ebfbf690f5876b4 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:32:23 +0100 Subject: [PATCH 7/9] feat(recaptcha): Don't allow pubkey without associated privkey --- src/core/config/check.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/config/check.rs b/src/core/config/check.rs index 8a3d1f77..6710a91a 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -180,11 +180,18 @@ 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_some() && config.recaptcha_private_site_key.is_some()) + && config.recaptcha_site_key.is_none() { return Err!(Config( "registration_token", From c362499ceffbc646633e310549085fdb3195ad6e Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:34:47 +0100 Subject: [PATCH 8/9] docs(recaptcha): Clarify registration when token & captcha are configured --- src/core/config/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 9df01d46..d93acd9b 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -557,8 +557,9 @@ pub struct Config { pub registration_token_file: Option, /// The public site key for reCaptcha. If this is provided, reCaptcha - /// becomes required during registration, **even if token registration is - /// enabled**. + /// 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 From b71186d9588a2b4998899cc2df99f0ab7e764bea Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 8 Jul 2025 19:47:42 +0100 Subject: [PATCH 9/9] chore(recaptcha): Update example config file Unsure how this managed to get past the `git commit -S -a` but sure --- conduwuit-example.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index c6301af5..bdc2f570 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -442,8 +442,9 @@ #registration_token_file = # The public site key for reCaptcha. If this is provided, reCaptcha -# becomes required during registration, **even if token registration is -# enabled**. +# 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