Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add support for alternative captchas #8797

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions changelog.d/8797.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for alternative captchas. Contributed by Mark Pugner.
93 changes: 88 additions & 5 deletions docs/CAPTCHA_SETUP.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
# 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 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

### Getting keys

Requires a site/secret key pair from:

<https://developers.google.com/recaptcha/>

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:
Expand All @@ -22,10 +30,85 @@ 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.

## altCaptcha

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:

<https://www.hcaptcha.com/> or <https://friendlycaptcha.com/>

### 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:

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 altCaptcha via:

enable_registration_altcaptcha: true

### Configuring IP used for user auth

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
listeners section of the homeserver.yaml configuration file.
53 changes: 50 additions & 3 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1044,16 +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 altCAPTCHA private key. Must be specified if
# enable_registration_altcaptcha is enabled.
#
#altcaptcha_private_key: "YOUR_PRIVATE_KEY"

# 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/[email protected]/widget.module.min.js or
# https://hcaptcha.com/1/api.js to use either FriendlyCaptcha or hCaptcha
#
#altcaptcha_template_script: "URL_TO_CAPTCHA_SCRIPT"

# 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'.
#
#enable_registration_altcaptcha: true


## TURN ##
Expand Down
1 change: 1 addition & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class LoginType:
EMAIL_IDENTITY = "m.login.email.identity"
MSISDN = "m.login.msisdn"
RECAPTCHA = "m.login.recaptcha"
ALTCAPTCHA = "org.matrix.msc2745.login.altcaptcha"
TERMS = "m.login.terms"
SSO = "m.login.sso"
DUMMY = "m.login.dummy"
Expand Down
79 changes: 76 additions & 3 deletions synapse/config/captcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,32 @@ def read_config(self, config, **kwargs):
self.recaptcha_template = self.read_templates(
["recaptcha.html"], autoescape=True
)[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.enable_registration_altcaptcha = config.get(
"enable_registration_altcaptcha", False
)
self.altcaptcha_siteverify_api = config.get(
"altcaptcha_siteverify_api", "https://hcaptcha.com/siteverify",
)
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 """\
Expand All @@ -47,14 +73,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 altCAPTCHA private key. Must be specified if
# enable_registration_altcaptcha is enabled.
#
#altcaptcha_private_key: "YOUR_PRIVATE_KEY"

# 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/[email protected]/widget.module.min.js or
# https://hcaptcha.com/1/api.js to use either FriendlyCaptcha or hCaptcha
#
#altcaptcha_template_script: "URL_TO_CAPTCHA_SCRIPT"

# 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'.
#
#enable_registration_altcaptcha: true
"""
6 changes: 6 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,11 @@ async def _check_auth_dict(
def _get_params_recaptcha(self) -> dict:
return {"public_key": self.hs.config.recaptcha_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 {
"policies": {
Expand All @@ -681,6 +686,7 @@ def _auth_dict_for_flows(

get_params = {
LoginType.RECAPTCHA: self._get_params_recaptcha,
LoginType.ALTCAPTCHA: self._get_params_altcaptcha,
LoginType.TERMS: self._get_params_terms,
}

Expand Down
62 changes: 62 additions & 0 deletions synapse/handlers/ui_auth/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,67 @@ async def check_auth(self, authdict, clientip):
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)


class AltcaptchaAuthChecker(UserInteractiveAuthChecker):
AUTH_TYPE = LoginType.ALTCAPTCHA

def __init__(self, hs):
super().__init__(hs)
self._enabled = bool(hs.config.altcaptcha_private_key)
self._http_client = hs.get_proxied_http_client()
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

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, "AltCaptcha response is required", errcode=Codes.CAPTCHA_NEEDED
)

logger.info(
"Submitting altcaptcha 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,
self._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.

logger.info(
"%s altCAPTCHA 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
Expand Down Expand Up @@ -239,6 +300,7 @@ async def check_auth(self, authdict, clientip):
DummyAuthChecker,
TermsAuthChecker,
RecaptchaAuthChecker,
AltcaptchaAuthChecker,
EmailIdentityAuthChecker,
MsisdnAuthChecker,
]
Expand Down
Loading