From ff7fbd904e68c665e465d1b7b44f16f9cf9929ed Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Sun, 22 Nov 2020 15:28:49 -0500 Subject: [PATCH 1/9] Adds support for hcaptcha Signed-off-by: Mark Pugner markpugner79@gmail.com --- changelog.d/8797.feature | 1 + synapse/api/constants.py | 1 + synapse/config/captcha.py | 32 +++++++++++++ synapse/handlers/auth.py | 4 ++ synapse/handlers/ui_auth/checkers.py | 61 ++++++++++++++++++++++++ synapse/res/templates/hcaptcha.html | 38 +++++++++++++++ synapse/rest/client/v2_alpha/auth.py | 30 ++++++++++++ synapse/rest/client/v2_alpha/register.py | 5 ++ 8 files changed, 172 insertions(+) create mode 100644 changelog.d/8797.feature create mode 100644 synapse/res/templates/hcaptcha.html diff --git a/changelog.d/8797.feature b/changelog.d/8797.feature new file mode 100644 index 000000000000..6c50f4b08aa9 --- /dev/null +++ b/changelog.d/8797.feature @@ -0,0 +1 @@ +Adds support for hCaptcha. Contributed by Mark Pugner. \ No newline at end of file diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 592abd844b8a..f4c65c97b8eb 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -60,6 +60,7 @@ class LoginType: EMAIL_IDENTITY = "m.login.email.identity" MSISDN = "m.login.msisdn" RECAPTCHA = "m.login.recaptcha" + HCAPTCHA = "m.login.hcaptcha" TERMS = "m.login.terms" SSO = "m.login.sso" DUMMY = "m.login.dummy" diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index cb009581651b..aad4180f2212 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -31,6 +31,17 @@ def read_config(self, config, **kwargs): self.recaptcha_template = self.read_templates( ["recaptcha.html"], autoescape=True )[0] + self.hcaptcha_private_key = config.get("hcaptcha_private_key") + self.hcaptcha_public_key = config.get("hcaptcha_public_key") + self.enable_registration_hcaptcha = config.get( + "enable_registration_hcaptcha", False + ) + self.hcaptcha_siteverify_api = config.get( + "hcaptcha_siteverify_api", "https://hcaptcha.com/siteverify", + ) + self.hcaptcha_template = self.read_templates( + ["hcaptcha.html"], autoescape=True + )[0] def generate_config_section(self, **kwargs): return """\ @@ -57,4 +68,25 @@ def generate_config_section(self, **kwargs): # Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify". # #recaptcha_siteverify_api: "https://my.recaptcha.site" + + # This homeserver's hCAPTCHA public key. Must be specified if + # enable_registration_hcaptcha is enabled. + # + #hcaptcha_public_key: "YOUR_PUBLIC_KEY" + + # This homeserver's hCAPTCHA private key. Must be specified if + # enable_registration_hcaptcha is enabled. + # + #hcaptcha_private_key: "YOUR_PRIVATE_KEY" + + # Uncomment to enable hCAPTCHA checks when registering, preventing signup + # unless a captcha is answered. Requires a valid hCaptcha + # public/private key. Defaults to 'false'. + # + #enable_registration_hcaptcha: true + + # The API endpoint to use for verifying m.login.recaptcha responses. + # Defaults to "https://my.hcaptcha.site". + # + #hcaptcha_siteverify_api: "https://my.hcaptcha.site" """ diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 5163afd86c66..54934d063145 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -655,6 +655,9 @@ async def _check_auth_dict( def _get_params_recaptcha(self) -> dict: return {"public_key": self.hs.config.recaptcha_public_key} + def _get_params_hcaptcha(self) -> dict: + return {"public_key": self.hs.config.hcaptcha_public_key} + def _get_params_terms(self) -> dict: return { "policies": { @@ -681,6 +684,7 @@ def _auth_dict_for_flows( get_params = { LoginType.RECAPTCHA: self._get_params_recaptcha, + LoginType.HCAPTCHA: self._get_params_hcaptcha, LoginType.TERMS: self._get_params_terms, } diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py index 3d66bf305e7f..9cf194dfcf50 100644 --- a/synapse/handlers/ui_auth/checkers.py +++ b/synapse/handlers/ui_auth/checkers.py @@ -132,6 +132,66 @@ async def check_auth(self, authdict, clientip): raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) +class HcaptchaAuthChecker(UserInteractiveAuthChecker): + AUTH_TYPE = LoginType.HCAPTCHA + + def __init__(self, hs): + super().__init__(hs) + self._enabled = bool(hs.config.hcaptcha_private_key) + self._http_client = hs.get_proxied_http_client() + self._url = hs.config.hcaptcha_siteverify_api + self._secret = hs.config.hcaptcha_private_key + + def is_enabled(self): + return self._enabled + + async def check_auth(self, authdict, clientip): + try: + user_response = authdict["response"] + except KeyError: + # Client tried to provide captcha but didn't give the parameter: + # bad request. + raise LoginError( + 400, "HCaptcha response is required", errcode=Codes.CAPTCHA_NEEDED + ) + + logger.info( + "Submitting hcaptcha response %s with remoteip %s", user_response, clientip + ) + + # TODO: get this from the homeserver rather than creating a new one for + # each request + try: + resp_body = await self._http_client.post_urlencoded_get_json( + self._url, + args={ + "secret": self._secret, + "response": user_response, + "remoteip": clientip, + }, + ) + except PartialDownloadError as pde: + # Twisted is silly + data = pde.response + resp_body = json_decoder.decode(data.decode("utf-8")) + + if "success" in resp_body: + # Note that we do NOT check the hostname here: we explicitly + # intend the CAPTCHA to be presented by whatever client the + # user is using, we just care that they have completed a CAPTCHA. + # this seems like where it would be nice to also integrate in support for hcaptcha + # https://www.hcaptcha.com/ + + logger.info( + "%s hCAPTCHA from hostname %s", + "Successful" if resp_body["success"] else "Failed", + resp_body.get("hostname"), + ) + if resp_body["success"]: + return True + raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) + + class _BaseThreepidAuthChecker: def __init__(self, hs): self.hs = hs @@ -239,6 +299,7 @@ async def check_auth(self, authdict, clientip): DummyAuthChecker, TermsAuthChecker, RecaptchaAuthChecker, + HcaptchaAuthChecker, EmailIdentityAuthChecker, MsisdnAuthChecker, ] diff --git a/synapse/res/templates/hcaptcha.html b/synapse/res/templates/hcaptcha.html new file mode 100644 index 000000000000..92c247f1f9d3 --- /dev/null +++ b/synapse/res/templates/hcaptcha.html @@ -0,0 +1,38 @@ + + +Authentication + + + + + + + +
+
+

+ Hello! We need to prevent computer programs and other automated + things from creating accounts on this server. +

+

+ Please verify that you're not a robot. +

+ +
+
+ +
+ +
+ + diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index fab077747f28..6d8b851b7a5f 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -58,6 +58,7 @@ def __init__(self, hs): self._cas_service_url = hs.config.cas_service_url self.recaptcha_template = hs.config.recaptcha_template + self.hcaptcha_template = hs.config.hcaptcha_template self.terms_template = hs.config.terms_template self.success_template = hs.config.fallback_success_template @@ -73,6 +74,13 @@ async def on_GET(self, request, stagetype): % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, ) + elif stagetype == LoginType.HCAPTCHA: + html = self.hcaptcha_template.render( + session=session, + myurl="%s/r0/auth/%s/fallback/web" + % (CLIENT_API_PREFIX, LoginType.HCAPTCHA), + sitekey=self.hs.config.hcaptcha_public_key, + ) elif stagetype == LoginType.TERMS: html = self.terms_template.render( session=session, @@ -146,6 +154,28 @@ async def on_POST(self, request, stagetype): % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, ) + elif stagetype == LoginType.HCAPTCHA: + response = parse_string(request, "h-captcha-response") + + if not response: + raise SynapseError(400, "No hcaptcha response supplied") + + authdict = {"response": response, "session": session} + + success = await self.auth_handler.add_oob_auth( + LoginType.HCAPTCHA, authdict, self.hs.get_ip_from_request(request) + ) + + if success: + html = self.success_template.render() + else: + + html = self.hcaptcha_template.render( + session=session, + myurl="%s/r0/auth/%s/fallback/web" + % (CLIENT_API_PREFIX, LoginType.HCAPTCHA), + sitekey=self.hs.config.hcaptcha_public_key, + ) elif stagetype == LoginType.TERMS: authdict = {"session": session} diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ea6811402650..9f48ab1d3786 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -780,6 +780,11 @@ def _calculate_registration_flows( for flow in flows: flow.insert(0, LoginType.RECAPTCHA) + # Prepend hcaptcha to all flows if we're requiring captcha + if config.enable_registration_hcaptcha: + for flow in flows: + flow.insert(0, LoginType.HCAPTCHA) + return flows From 387ba4d834fed85d3bd21b0218e01b627ad71ddf Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Sun, 22 Nov 2020 15:39:19 -0500 Subject: [PATCH 2/9] Updates sample config for hcaptcha --- docs/sample_config.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 52a1d8b85315..c02b6d6e9bc3 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1055,6 +1055,27 @@ url_preview_accept_language: # #recaptcha_siteverify_api: "https://my.recaptcha.site" +# This homeserver's hCAPTCHA public key. Must be specified if +# enable_registration_hcaptcha is enabled. +# +#hcaptcha_public_key: "YOUR_PUBLIC_KEY" + +# This homeserver's hCAPTCHA private key. Must be specified if +# enable_registration_hcaptcha is enabled. +# +#hcaptcha_private_key: "YOUR_PRIVATE_KEY" + +# Uncomment to enable hCAPTCHA checks when registering, preventing signup +# unless a captcha is answered. Requires a valid hCaptcha +# public/private key. Defaults to 'false'. +# +#enable_registration_hcaptcha: true + +# The API endpoint to use for verifying m.login.recaptcha responses. +# Defaults to "https://my.hcaptcha.site". +# +#hcaptcha_siteverify_api: "https://my.hcaptcha.site" + ## TURN ## From ff923c86f4108a72423c1ab99a5ec1e089a1c1dc Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Sun, 22 Nov 2020 18:50:09 -0500 Subject: [PATCH 3/9] Updates captcha documentation to reflect changes. --- docs/CAPTCHA_SETUP.md | 44 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/CAPTCHA_SETUP.md b/docs/CAPTCHA_SETUP.md index 331e5d059a0e..26b58b463b7c 100644 --- a/docs/CAPTCHA_SETUP.md +++ b/docs/CAPTCHA_SETUP.md @@ -1,8 +1,14 @@ # Overview -Captcha can be enabled for this home server. This file explains how to do that. -The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google. -## Getting keys +Captchas can be enabled for this home server. This file explains how to do that. +The captcha mechanism supports Google's ReCaptcha and or hCaptcha. This requires +API keys from Google or hCaptcha. You can also enable both captcha systems to work +at the same time. If they are both enabled hCaptcha will show first and then +ReCaptcha will become active after hCaptcha has been solved. + +## ReCaptcha + +### Getting keys Requires a site/secret key pair from: @@ -10,7 +16,7 @@ Requires a site/secret key pair from: Must be a reCAPTCHA v2 key using the "I'm not a robot" Checkbox option -## Setting ReCaptcha Keys +### Setting ReCaptcha Keys The keys are a config option on the home server config. If they are not visible, you can generate them via `--generate-config`. Set the following value: @@ -22,10 +28,38 @@ In addition, you MUST enable captchas via: enable_registration_captcha: true -## Configuring IP used for auth +### Configuring IP used for auth The ReCaptcha API requires that the IP address of the user who solved the captcha is sent. If the client is connecting through a proxy or load balancer, it may be required to use the `X-Forwarded-For` (XFF) header instead of the origin IP address. This can be configured using the `x_forwarded` directive in the listeners section of the homeserver.yaml configuration file. + +## hCaptcha + +### Getting hCaptcha keys + +Requires a site/secret key pair from: + + + +### Setting hCaptcha Keys + +The keys are a config option on the home server config. If they are not +visible, you can generate them via `--generate-config`. Set the following value: + + hcaptcha_public_key: YOUR_SITE_KEY + hcaptcha_private_key: YOUR_SECRET_KEY + +In addition, you MUST enable hCaptcha via: + + enable_registration_hcaptcha: true + +### Configuring IP used for user auth + +The hCaptcha API requires that the IP address of the user who solved the +captcha is sent. If the client is connecting through a proxy or load balancer, +it may be required to use the `X-Forwarded-For` (XFF) header instead of the origin +IP address. This can be configured using the `x_forwarded` directive in the +listeners section of the homeserver.yaml configuration file. From 2795519a7eef2961543024179c1340b20e72088e Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Sun, 13 Dec 2020 16:24:04 -0500 Subject: [PATCH 4/9] Updates the hcaptcha constant to reflect that it is not part of the matrix auth standard yet and is related to msc2745. --- synapse/api/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index f4c65c97b8eb..024251cf98b2 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -60,7 +60,7 @@ class LoginType: EMAIL_IDENTITY = "m.login.email.identity" MSISDN = "m.login.msisdn" RECAPTCHA = "m.login.recaptcha" - HCAPTCHA = "m.login.hcaptcha" + HCAPTCHA = "org.matrix.msc2745.login.hcaptcha" TERMS = "m.login.terms" SSO = "m.login.sso" DUMMY = "m.login.dummy" From 7b97fca34008b0f66a711f893ead40c626fbc6ba Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Thu, 17 Dec 2020 17:46:47 -0500 Subject: [PATCH 5/9] Renames from hcaptcha to altcaptcha and extends features to template out generic support. --- docs/CAPTCHA_SETUP.md | 75 +++++++++++++++++---- docs/sample_config.yaml | 58 ++++++++++++----- synapse/api/constants.py | 2 +- synapse/config/captcha.py | 83 +++++++++++++++++------- synapse/res/templates/altcaptcha.html | 40 ++++++++++++ synapse/rest/client/v2_alpha/auth.py | 34 ++++++---- synapse/rest/client/v2_alpha/register.py | 9 ++- 7 files changed, 229 insertions(+), 72 deletions(-) create mode 100644 synapse/res/templates/altcaptcha.html diff --git a/docs/CAPTCHA_SETUP.md b/docs/CAPTCHA_SETUP.md index 26b58b463b7c..4834c3216683 100644 --- a/docs/CAPTCHA_SETUP.md +++ b/docs/CAPTCHA_SETUP.md @@ -1,10 +1,12 @@ # Overview Captchas can be enabled for this home server. This file explains how to do that. -The captcha mechanism supports Google's ReCaptcha and or hCaptcha. This requires -API keys from Google or hCaptcha. You can also enable both captcha systems to work -at the same time. If they are both enabled hCaptcha will show first and then -ReCaptcha will become active after hCaptcha has been solved. +The captcha mechanism supports Google's ReCaptcha and or an alternative captcha +such as hCaptcha, Friendly Captcha, or similar implentations. This requires +API keys from Google or the alternative captcha service. You can also enable both +captcha systems to work at the same time. If they are both enabled the alternative +captcha will prompt "Start Authentication" first which will open a new window and +then ReCaptcha will become active after the alternate Captcha has been solved. ## ReCaptcha @@ -36,29 +38,76 @@ it may be required to use the `X-Forwarded-For` (XFF) header instead of the orig IP address. This can be configured using the `x_forwarded` directive in the listeners section of the homeserver.yaml configuration file. -## hCaptcha +## altCaptcha -### Getting hCaptcha keys +Alternative captcha's are supported that: + 1) Use no more than 2 scripts to be imbeded. + 2) Use a post request API call to implement captcha validation. + +By default altCaptcha provides default settings set to hCaptcha, but can be +configured to use similar captcha implentations like Friendly Captcha. + +### Getting altCaptcha keys Requires a site/secret key pair from: - + or -### Setting hCaptcha Keys +### Setting altCaptcha Keys The keys are a config option on the home server config. If they are not visible, you can generate them via `--generate-config`. Set the following value: - hcaptcha_public_key: YOUR_SITE_KEY - hcaptcha_private_key: YOUR_SECRET_KEY + altcaptcha_public_key: YOUR_SITE_KEY + altcaptcha_private_key: YOUR_SECRET_KEY + +### Scripts to embed and other settings + +To use an alternative captcha that is not hCaptcha you will need to customize +the following settings as well. + +#### Template Embed Scripts + +The template embed scripts are used to customize the scripts that will be embed +in the fallback altcaptcha.html template page. These are the dependencies required +to load the desired captcha. Some modern systems use a WASM/Module script to +implement advanced captcha functionality and need to be specified via one of the +two altcaptcha_template_script configuration options. +The default value is "https://hcaptcha.com/1/api.js". +If altcaptcha_template_script2 is not configured it is set to the value of +altcaptcha_template_script to avoid 404 errors in the second script tag. + + altcaptcha_template_script: URL_TO_CAPTCHA_SCRIPT_TO_EMBED + altcaptcha_template_script2: URL_TO_CAPTCHA_SCRIPT2_TO_EMBED_WASM_MODULE + +#### Additional template settings + +For embedding the alternative captcha you also need to specify the div class that +it binds to for display using the altcaptcha_callback_class_target +Examples: "h-captcha", "frc-captcha" +The default value is "h-captcha". + + altcaptcha_callback_class_target: ALT_CAPTCHA_BINDING_CLASS + +The name of the captcha response also needs to be configured for alternative captchas. +This effects the synapse/api/client/v2_alpha/auth.py API endpoint data parsing. +The default value is "h-captcha-response". + + altcaptcha_response_template: NAME_OF_RESPONSE + +The name of the solution parameter to submit for validation may also need to be +configured. For example to use Friendly capatcha the value needed is "solution". +The default value is "response". + + altcaptcha_siteverify_api_response: PARAMETER_NAME_FOR_API_CAPTCHA_VALIDATION -In addition, you MUST enable hCaptcha via: +In addition, you MUST enable altCaptcha via: - enable_registration_hcaptcha: true + enable_registration_altcaptcha: true ### Configuring IP used for user auth -The hCaptcha API requires that the IP address of the user who solved the +Most Captcha API's require that the IP address of the user who solved the captcha is sent. If the client is connecting through a proxy or load balancer, it may be required to use the `X-Forwarded-For` (XFF) header instead of the origin IP address. This can be configured using the `x_forwarded` directive in the diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index c02b6d6e9bc3..20f0f95ff9a6 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1044,37 +1044,63 @@ url_preview_accept_language: # #recaptcha_private_key: "YOUR_PRIVATE_KEY" +# The API endpoint to use for verifying m.login.recaptcha responses. +# Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify". +# +#recaptcha_siteverify_api: "https://my.recaptcha.site" + # Uncomment to enable ReCaptcha checks when registering, preventing signup # unless a captcha is answered. Requires a valid ReCaptcha # public/private key. Defaults to 'false'. # #enable_registration_captcha: true -# The API endpoint to use for verifying m.login.recaptcha responses. -# Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify". +## altCaptcha ## +# See docs/CAPTCHA_SETUP.md for full details of configuring this. +# This homeserver's altCAPTCHA public key. Must be specified if +# enable_registration_altcaptcha is enabled. # -#recaptcha_siteverify_api: "https://my.recaptcha.site" +#altcaptcha_public_key: "YOUR_PUBLIC_KEY" -# This homeserver's hCAPTCHA public key. Must be specified if -# enable_registration_hcaptcha is enabled. +# This homeserver's altCAPTCHA private key. Must be specified if +# enable_registration_altcaptcha is enabled. # -#hcaptcha_public_key: "YOUR_PUBLIC_KEY" +#altcaptcha_private_key: "YOUR_PRIVATE_KEY" -# This homeserver's hCAPTCHA private key. Must be specified if -# enable_registration_hcaptcha is enabled. +# This is the callback class used for altcaptcha validation, it is an +# implementation specific detail used in the altcaptcha page for result. +# validation. Example: "frc-captcha" or "h-captcha" # -#hcaptcha_private_key: "YOUR_PRIVATE_KEY" +#altcaptcha_callback_class_target: "IMPLEMENTATION_SPECIFIC_CALLBACK_CLASS_TARGET" -# Uncomment to enable hCAPTCHA checks when registering, preventing signup -# unless a captcha is answered. Requires a valid hCaptcha -# public/private key. Defaults to 'false'. +# This is the captcha script used in the template altcaptcha page. +# Example: https://cdn.jsdelivr.net/npm/friendly-challenge@0.6.1/widget.module.min.js or +# https://hcaptcha.com/1/api.js to use either FriendlyCaptcha or hCaptcha # -#enable_registration_hcaptcha: true +#altcaptcha_template_script: "URL_TO_CAPTCHA_SCRIPT" -# The API endpoint to use for verifying m.login.recaptcha responses. -# Defaults to "https://my.hcaptcha.site". +# The API endpoint to use for verifying org.matrix.msc2745.login.altcaptcha responses. +# Defaults to "https://hcaptcha.com/siteverify". +# +#altcaptcha_siteverify_api: "https://hcaptcha.com/siteverify" + +# This value is used for sending the altcaptcha to validate via the api +# For some alternative captcha's they may use "solution" to validate +# The default value for this is "response" +# +#altcaptcha_siteverify_api_response: "response" + +# This is the response name used for the captcha system you have configured +# Used in synapse/rest/client/v2_alpha/auth.py +# Example: "frc-captcha-solution" or "h-captcha-response" +# +#altcaptcha_response_template: "CAPTCHA_RESPONSE_TEMPLATE" + +# Uncomment to enable altcaptcha checks when registering, preventing signup +# unless a captcha is answered. Requires a valid altcaptcha +# public/private key. Defaults to 'false'. # -#hcaptcha_siteverify_api: "https://my.hcaptcha.site" +#enable_registration_altcaptcha: true ## TURN ## diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 024251cf98b2..51d510a5c2f0 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -60,7 +60,7 @@ class LoginType: EMAIL_IDENTITY = "m.login.email.identity" MSISDN = "m.login.msisdn" RECAPTCHA = "m.login.recaptcha" - HCAPTCHA = "org.matrix.msc2745.login.hcaptcha" + ALTCAPTCHA = "org.matrix.msc2745.login.altcaptcha" TERMS = "m.login.terms" SSO = "m.login.sso" DUMMY = "m.login.dummy" diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index aad4180f2212..e2b8d8dd6461 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -31,18 +31,25 @@ def read_config(self, config, **kwargs): self.recaptcha_template = self.read_templates( ["recaptcha.html"], autoescape=True )[0] - self.hcaptcha_private_key = config.get("hcaptcha_private_key") - self.hcaptcha_public_key = config.get("hcaptcha_public_key") - self.enable_registration_hcaptcha = config.get( - "enable_registration_hcaptcha", False + self.altcaptcha_private_key = config.get("altcaptcha_private_key") + self.altcaptcha_public_key = config.get("altcaptcha_public_key") + self.altcaptcha_callback_class_target = config.get("altcaptcha_callback_class_target","h-captcha") + self.altcaptcha_template_script = config.get("altcaptcha_template_script","https://hcaptcha.com/1/api.js") + self.altcaptcha_template_script2 = config.get("altcaptcha_template_script2", self.altcaptcha_template_script) + self.altcaptcha_response_template = config.get("altcaptcha_response_template","h-captcha-response") + self.enable_registration_altcaptcha = config.get( + "enable_registration_altcaptcha", False ) - self.hcaptcha_siteverify_api = config.get( - "hcaptcha_siteverify_api", "https://hcaptcha.com/siteverify", + self.altcaptcha_siteverify_api = config.get( + "altcaptcha_siteverify_api", "https://hcaptcha.com/siteverify", ) - self.hcaptcha_template = self.read_templates( - ["hcaptcha.html"], autoescape=True + self.altcaptcha_siteverify_api_response = config.get( + "altcaptcha_siteverify_api_response", "response", + ) + self.altcaptcha_template = self.read_templates( + ["altcaptcha.html"], autoescape=True )[0] - + def generate_config_section(self, **kwargs): return """\ ## Captcha ## @@ -58,35 +65,61 @@ def generate_config_section(self, **kwargs): # #recaptcha_private_key: "YOUR_PRIVATE_KEY" + # The API endpoint to use for verifying m.login.recaptcha responses. + # Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify". + # + #recaptcha_siteverify_api: "https://my.recaptcha.site" + # Uncomment to enable ReCaptcha checks when registering, preventing signup # unless a captcha is answered. Requires a valid ReCaptcha # public/private key. Defaults to 'false'. # #enable_registration_captcha: true - # The API endpoint to use for verifying m.login.recaptcha responses. - # Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify". + ## altCaptcha ## + # See docs/CAPTCHA_SETUP.md for full details of configuring this. + # This homeserver's altCAPTCHA public key. Must be specified if + # enable_registration_altcaptcha is enabled. # - #recaptcha_siteverify_api: "https://my.recaptcha.site" + #altcaptcha_public_key: "YOUR_PUBLIC_KEY" - # This homeserver's hCAPTCHA public key. Must be specified if - # enable_registration_hcaptcha is enabled. + # This homeserver's altCAPTCHA private key. Must be specified if + # enable_registration_altcaptcha is enabled. # - #hcaptcha_public_key: "YOUR_PUBLIC_KEY" + #altcaptcha_private_key: "YOUR_PRIVATE_KEY" - # This homeserver's hCAPTCHA private key. Must be specified if - # enable_registration_hcaptcha is enabled. + # This is the callback class used for altcaptcha validation, it is an + # implementation specific detail used in the altcaptcha page for result. + # validation. Example: "frc-captcha" or "h-captcha" # - #hcaptcha_private_key: "YOUR_PRIVATE_KEY" + #altcaptcha_callback_class_target: "IMPLEMENTATION_SPECIFIC_CALLBACK_CLASS_TARGET" - # Uncomment to enable hCAPTCHA checks when registering, preventing signup - # unless a captcha is answered. Requires a valid hCaptcha - # public/private key. Defaults to 'false'. + # This is the captcha script used in the template altcaptcha page. + # Example: https://cdn.jsdelivr.net/npm/friendly-challenge@0.6.1/widget.module.min.js or + # https://hcaptcha.com/1/api.js to use either FriendlyCaptcha or hCaptcha # - #enable_registration_hcaptcha: true + #altcaptcha_template_script: "URL_TO_CAPTCHA_SCRIPT" - # The API endpoint to use for verifying m.login.recaptcha responses. - # Defaults to "https://my.hcaptcha.site". + # The API endpoint to use for verifying org.matrix.msc2745.login.altcaptcha responses. + # Defaults to "https://hcaptcha.com/siteverify". + # + #altcaptcha_siteverify_api: "https://hcaptcha.com/siteverify" + + # This value is used for sending the altcaptcha to validate via the api + # For some alternative captcha's they may use "solution" to validate + # The default value for this is "response" + # + #altcaptcha_siteverify_api_response: "response" + + # This is the response name used for the captcha system you have configured + # Used in synapse/rest/client/v2_alpha/auth.py + # Example: "frc-captcha-solution" or "h-captcha-response" + # + #altcaptcha_response_template: "CAPTCHA_RESPONSE_TEMPLATE" + + # Uncomment to enable altcaptcha checks when registering, preventing signup + # unless a captcha is answered. Requires a valid altcaptcha + # public/private key. Defaults to 'false'. # - #hcaptcha_siteverify_api: "https://my.hcaptcha.site" + #enable_registration_altcaptcha: true """ diff --git a/synapse/res/templates/altcaptcha.html b/synapse/res/templates/altcaptcha.html new file mode 100644 index 000000000000..df39d02da6a7 --- /dev/null +++ b/synapse/res/templates/altcaptcha.html @@ -0,0 +1,40 @@ + + +Authentication + + + + + + + + +
+
+

+ Hello! We need to prevent computer programs and other automated + things from creating accounts on this server. +

+

+ Please verify that you're not a robot. +

+ +
+
+ +
+ +
+ + diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 6d8b851b7a5f..bc50fc5ec602 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -58,7 +58,13 @@ def __init__(self, hs): self._cas_service_url = hs.config.cas_service_url self.recaptcha_template = hs.config.recaptcha_template - self.hcaptcha_template = hs.config.hcaptcha_template + self.altcaptcha_template = hs.config.altcaptcha_template + self.altcaptcha_response_template = hs.config.altcaptcha_response_template + self.altcaptcha_callback_class_target = hs.config.altcaptcha_callback_class_target + self.altcaptcha_template_script = hs.config.altcaptcha_template_script + self.altcaptcha_template_script2 = hs.config.altcaptcha_template_script2 + self.altcaptcha_siteverify_api = hs.config.altcaptcha_siteverify_api + self.terms_template = hs.config.terms_template self.success_template = hs.config.fallback_success_template @@ -74,12 +80,16 @@ async def on_GET(self, request, stagetype): % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, ) - elif stagetype == LoginType.HCAPTCHA: - html = self.hcaptcha_template.render( + elif stagetype == LoginType.ALTCAPTCHA: + html = self.altcaptcha_template.render( session=session, myurl="%s/r0/auth/%s/fallback/web" - % (CLIENT_API_PREFIX, LoginType.HCAPTCHA), - sitekey=self.hs.config.hcaptcha_public_key, + % (CLIENT_API_PREFIX, LoginType.ALTCAPTCHA), + sitekey=self.hs.config.altcaptcha_public_key, + altcaptcha_callback_class_target=self.hs.config.altcaptcha_callback_class_target, + altcaptcha_template_script=self.hs.config.altcaptcha_template_script, + altcaptcha_template_script2=self.hs.config.altcaptcha_template_script2, + ) elif stagetype == LoginType.TERMS: html = self.terms_template.render( @@ -153,17 +163,17 @@ async def on_POST(self, request, stagetype): myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, - ) - elif stagetype == LoginType.HCAPTCHA: - response = parse_string(request, "h-captcha-response") + ) + elif stagetype == LoginType.ALTCAPTCHA: + response = parse_string(request, self.altcaptcha_response_template) if not response: - raise SynapseError(400, "No hcaptcha response supplied") + raise SynapseError(400, "No altcaptcha response supplied") authdict = {"response": response, "session": session} success = await self.auth_handler.add_oob_auth( - LoginType.HCAPTCHA, authdict, self.hs.get_ip_from_request(request) + LoginType.ALTCAPTCHA, authdict, self.hs.get_ip_from_request(request) ) if success: @@ -173,8 +183,8 @@ async def on_POST(self, request, stagetype): html = self.hcaptcha_template.render( session=session, myurl="%s/r0/auth/%s/fallback/web" - % (CLIENT_API_PREFIX, LoginType.HCAPTCHA), - sitekey=self.hs.config.hcaptcha_public_key, + % (CLIENT_API_PREFIX, LoginType.ALTCAPTCHA), + sitekey=self.hs.config.altcaptcha_public_key, ) elif stagetype == LoginType.TERMS: authdict = {"session": session} diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 9f48ab1d3786..ef166ee10705 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -779,12 +779,11 @@ def _calculate_registration_flows( if config.enable_registration_captcha: for flow in flows: flow.insert(0, LoginType.RECAPTCHA) - - # Prepend hcaptcha to all flows if we're requiring captcha - if config.enable_registration_hcaptcha: + + # Prepend altcaptcha to all flows if we're requiring altcaptcha + if config.enable_registration_altcaptcha: for flow in flows: - flow.insert(0, LoginType.HCAPTCHA) - + flow.insert(0, LoginType.ALTCAPTCHA) return flows From a81dda1df3d9f5582703c4750fec6dbbc1fceacd Mon Sep 17 00:00:00 2001 From: MarkPugner79 <74796294+MarkPugner79@users.noreply.github.com> Date: Thu, 17 Dec 2020 17:52:39 -0500 Subject: [PATCH 6/9] Delete hcaptcha.html This should be removed for the PR as it is no longer used. --- synapse/res/templates/hcaptcha.html | 38 ----------------------------- 1 file changed, 38 deletions(-) delete mode 100644 synapse/res/templates/hcaptcha.html diff --git a/synapse/res/templates/hcaptcha.html b/synapse/res/templates/hcaptcha.html deleted file mode 100644 index 92c247f1f9d3..000000000000 --- a/synapse/res/templates/hcaptcha.html +++ /dev/null @@ -1,38 +0,0 @@ - - -Authentication - - - - - - - -
-
-

- Hello! We need to prevent computer programs and other automated - things from creating accounts on this server. -

-

- Please verify that you're not a robot. -

- -
-
- -
- -
- - From 6cf9b6e2300ff4e02628b387c45f1c27e6b2374b Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Thu, 17 Dec 2020 18:11:23 -0500 Subject: [PATCH 7/9] Fixed and ran the linter on the files. --- synapse/config/captcha.py | 22 ++++++++++++++------- synapse/handlers/auth.py | 8 +++++--- synapse/handlers/ui_auth/checkers.py | 25 ++++++++++++------------ synapse/rest/client/v2_alpha/auth.py | 7 ++++--- synapse/rest/client/v2_alpha/register.py | 2 +- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index e2b8d8dd6461..396d17b0a6dc 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -33,10 +33,18 @@ def read_config(self, config, **kwargs): )[0] self.altcaptcha_private_key = config.get("altcaptcha_private_key") self.altcaptcha_public_key = config.get("altcaptcha_public_key") - self.altcaptcha_callback_class_target = config.get("altcaptcha_callback_class_target","h-captcha") - self.altcaptcha_template_script = config.get("altcaptcha_template_script","https://hcaptcha.com/1/api.js") - self.altcaptcha_template_script2 = config.get("altcaptcha_template_script2", self.altcaptcha_template_script) - self.altcaptcha_response_template = config.get("altcaptcha_response_template","h-captcha-response") + self.altcaptcha_callback_class_target = config.get( + "altcaptcha_callback_class_target", "h-captcha" + ) + self.altcaptcha_template_script = config.get( + "altcaptcha_template_script", "https://hcaptcha.com/1/api.js" + ) + self.altcaptcha_template_script2 = config.get( + "altcaptcha_template_script2", self.altcaptcha_template_script + ) + self.altcaptcha_response_template = config.get( + "altcaptcha_response_template", "h-captcha-response" + ) self.enable_registration_altcaptcha = config.get( "enable_registration_altcaptcha", False ) @@ -49,7 +57,7 @@ def read_config(self, config, **kwargs): self.altcaptcha_template = self.read_templates( ["altcaptcha.html"], autoescape=True )[0] - + def generate_config_section(self, **kwargs): return """\ ## Captcha ## @@ -88,14 +96,14 @@ def generate_config_section(self, **kwargs): # #altcaptcha_private_key: "YOUR_PRIVATE_KEY" - # This is the callback class used for altcaptcha validation, it is an + # This is the callback class used for altcaptcha validation, it is an # implementation specific detail used in the altcaptcha page for result. # validation. Example: "frc-captcha" or "h-captcha" # #altcaptcha_callback_class_target: "IMPLEMENTATION_SPECIFIC_CALLBACK_CLASS_TARGET" # This is the captcha script used in the template altcaptcha page. - # Example: https://cdn.jsdelivr.net/npm/friendly-challenge@0.6.1/widget.module.min.js or + # Example: https://cdn.jsdelivr.net/npm/friendly-challenge@0.6.1/widget.module.min.js or # https://hcaptcha.com/1/api.js to use either FriendlyCaptcha or hCaptcha # #altcaptcha_template_script: "URL_TO_CAPTCHA_SCRIPT" diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 54934d063145..0dc9eddd7adf 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -655,8 +655,10 @@ async def _check_auth_dict( def _get_params_recaptcha(self) -> dict: return {"public_key": self.hs.config.recaptcha_public_key} - def _get_params_hcaptcha(self) -> dict: - return {"public_key": self.hs.config.hcaptcha_public_key} + # This is probably where we would hook for the non fallback workflows to extend + # The frontends to support configurable captchas + def _get_params_altcaptcha(self) -> dict: + return {"public_key": self.hs.config.altcaptcha_public_key} def _get_params_terms(self) -> dict: return { @@ -684,7 +686,7 @@ def _auth_dict_for_flows( get_params = { LoginType.RECAPTCHA: self._get_params_recaptcha, - LoginType.HCAPTCHA: self._get_params_hcaptcha, + LoginType.ALTCAPTCHA: self._get_params_altcaptcha, LoginType.TERMS: self._get_params_terms, } diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py index 9cf194dfcf50..d95d815f1641 100644 --- a/synapse/handlers/ui_auth/checkers.py +++ b/synapse/handlers/ui_auth/checkers.py @@ -132,15 +132,16 @@ async def check_auth(self, authdict, clientip): raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) -class HcaptchaAuthChecker(UserInteractiveAuthChecker): - AUTH_TYPE = LoginType.HCAPTCHA +class AltcaptchaAuthChecker(UserInteractiveAuthChecker): + AUTH_TYPE = LoginType.ALTCAPTCHA def __init__(self, hs): super().__init__(hs) - self._enabled = bool(hs.config.hcaptcha_private_key) + self._enabled = bool(hs.config.altcaptcha_private_key) self._http_client = hs.get_proxied_http_client() - self._url = hs.config.hcaptcha_siteverify_api - self._secret = hs.config.hcaptcha_private_key + self._url = hs.config.altcaptcha_siteverify_api + self._secret = hs.config.altcaptcha_private_key + self._response = hs.config.altcaptcha_siteverify_api_response def is_enabled(self): return self._enabled @@ -152,11 +153,13 @@ async def check_auth(self, authdict, clientip): # Client tried to provide captcha but didn't give the parameter: # bad request. raise LoginError( - 400, "HCaptcha response is required", errcode=Codes.CAPTCHA_NEEDED + 400, "AltCaptcha response is required", errcode=Codes.CAPTCHA_NEEDED ) logger.info( - "Submitting hcaptcha response %s with remoteip %s", user_response, clientip + "Submitting altcaptcha response %s with remoteip %s", + user_response, + clientip, ) # TODO: get this from the homeserver rather than creating a new one for @@ -166,7 +169,7 @@ async def check_auth(self, authdict, clientip): self._url, args={ "secret": self._secret, - "response": user_response, + self._response: user_response, "remoteip": clientip, }, ) @@ -179,11 +182,9 @@ async def check_auth(self, authdict, clientip): # Note that we do NOT check the hostname here: we explicitly # intend the CAPTCHA to be presented by whatever client the # user is using, we just care that they have completed a CAPTCHA. - # this seems like where it would be nice to also integrate in support for hcaptcha - # https://www.hcaptcha.com/ logger.info( - "%s hCAPTCHA from hostname %s", + "%s altCAPTCHA from hostname %s", "Successful" if resp_body["success"] else "Failed", resp_body.get("hostname"), ) @@ -299,7 +300,7 @@ async def check_auth(self, authdict, clientip): DummyAuthChecker, TermsAuthChecker, RecaptchaAuthChecker, - HcaptchaAuthChecker, + AltcaptchaAuthChecker, EmailIdentityAuthChecker, MsisdnAuthChecker, ] diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index bc50fc5ec602..f2582226c2cb 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -60,7 +60,9 @@ def __init__(self, hs): self.recaptcha_template = hs.config.recaptcha_template self.altcaptcha_template = hs.config.altcaptcha_template self.altcaptcha_response_template = hs.config.altcaptcha_response_template - self.altcaptcha_callback_class_target = hs.config.altcaptcha_callback_class_target + self.altcaptcha_callback_class_target = ( + hs.config.altcaptcha_callback_class_target + ) self.altcaptcha_template_script = hs.config.altcaptcha_template_script self.altcaptcha_template_script2 = hs.config.altcaptcha_template_script2 self.altcaptcha_siteverify_api = hs.config.altcaptcha_siteverify_api @@ -89,7 +91,6 @@ async def on_GET(self, request, stagetype): altcaptcha_callback_class_target=self.hs.config.altcaptcha_callback_class_target, altcaptcha_template_script=self.hs.config.altcaptcha_template_script, altcaptcha_template_script2=self.hs.config.altcaptcha_template_script2, - ) elif stagetype == LoginType.TERMS: html = self.terms_template.render( @@ -163,7 +164,7 @@ async def on_POST(self, request, stagetype): myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, - ) + ) elif stagetype == LoginType.ALTCAPTCHA: response = parse_string(request, self.altcaptcha_response_template) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ef166ee10705..e2da439859ab 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -779,7 +779,7 @@ def _calculate_registration_flows( if config.enable_registration_captcha: for flow in flows: flow.insert(0, LoginType.RECAPTCHA) - + # Prepend altcaptcha to all flows if we're requiring altcaptcha if config.enable_registration_altcaptcha: for flow in flows: From 100b9c6f64e745a9b8ffd26adcbb393b2092da39 Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Thu, 17 Dec 2020 18:13:03 -0500 Subject: [PATCH 8/9] Updates the changelog feature description. --- changelog.d/8797.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/8797.feature b/changelog.d/8797.feature index 6c50f4b08aa9..07159ed22618 100644 --- a/changelog.d/8797.feature +++ b/changelog.d/8797.feature @@ -1 +1 @@ -Adds support for hCaptcha. Contributed by Mark Pugner. \ No newline at end of file +Add support for alternative captchas. Contributed by Mark Pugner. \ No newline at end of file From 318a9eec5869e39281c92e378ee854028605a54b Mon Sep 17 00:00:00 2001 From: Mark Pugner Date: Thu, 17 Dec 2020 18:36:37 -0500 Subject: [PATCH 9/9] Fixes sample config and variable name error. --- docs/sample_config.yaml | 4 ++-- synapse/rest/client/v2_alpha/auth.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 20f0f95ff9a6..d64ad587d4be 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1067,14 +1067,14 @@ url_preview_accept_language: # #altcaptcha_private_key: "YOUR_PRIVATE_KEY" -# This is the callback class used for altcaptcha validation, it is an +# This is the callback class used for altcaptcha validation, it is an # implementation specific detail used in the altcaptcha page for result. # validation. Example: "frc-captcha" or "h-captcha" # #altcaptcha_callback_class_target: "IMPLEMENTATION_SPECIFIC_CALLBACK_CLASS_TARGET" # This is the captcha script used in the template altcaptcha page. -# Example: https://cdn.jsdelivr.net/npm/friendly-challenge@0.6.1/widget.module.min.js or +# Example: https://cdn.jsdelivr.net/npm/friendly-challenge@0.6.1/widget.module.min.js or # https://hcaptcha.com/1/api.js to use either FriendlyCaptcha or hCaptcha # #altcaptcha_template_script: "URL_TO_CAPTCHA_SCRIPT" diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index f2582226c2cb..e00244d4b916 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -181,7 +181,7 @@ async def on_POST(self, request, stagetype): html = self.success_template.render() else: - html = self.hcaptcha_template.render( + html = self.altcaptcha_template.render( session=session, myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.ALTCAPTCHA),