Skip to content

Commit

Permalink
Add Cloudflare Turnstile challenge to signup and password reset (#1427)
Browse files Browse the repository at this point in the history
  • Loading branch information
charmander authored Sep 10, 2024
2 parents 2fb5fba + 5cd2792 commit fdbe41a
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 3 deletions.
5 changes: 5 additions & 0 deletions config/site.config.txt.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ host = localhost
# See https://cryptography.io/en/latest/fernet/ -- Fernet.generate_key()
secret_key = 2iY4trxnpmNLlQifnQ21pFF0nb-VlmpxRUI6W_uP1oQ=

[turnstile]
site_key = 3x00000000000000000000FF
secret_key = 1x0000000000000000000000000000000AA
enforce = true

# These keys MUST be changed when in production
[secret_keys]
# can be generated with `secrets.token_urlsafe(32) + "="`
Expand Down
11 changes: 11 additions & 0 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from gunicorn.glogging import CONFIG_DEFAULTS as _LOGCONFIG_DEFAULTS
from prometheus_client import multiprocess


Expand All @@ -12,6 +13,16 @@
}
forwarded_allow_ips = '*'

logconfig_dict = {
"loggers": {
**_LOGCONFIG_DEFAULTS["loggers"],
"weasyl": {
"level": "DEBUG",
"handlers": ["console"],
},
},
}


def child_exit(server, worker):
multiprocess.mark_process_dead(worker.pid)
5 changes: 5 additions & 0 deletions weasyl/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
moderation,
profile,
resetpassword,
turnstile,
two_factor_auth,
)
from weasyl.controllers.decorators import (
Expand Down Expand Up @@ -185,6 +186,8 @@ def signup_get_(request):
@guest_required
@token_checked
def signup_post_(request):
turnstile.require(request)

form = request.web_input(
username="", password="", email="")

Expand Down Expand Up @@ -227,6 +230,8 @@ def forgotpassword_get_(request):
@guest_required
@token_checked
def forgetpassword_post_(request):
turnstile.require(request)

resetpassword.request(email=request.POST['email'])
return Response(define.errorpage(
request.userid,
Expand Down
2 changes: 2 additions & 0 deletions weasyl/define.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from weasyl import errorcode
from weasyl import macro
from weasyl import metrics
from weasyl import turnstile
from weasyl.config import config_obj, config_read_setting
from weasyl.error import WeasylError

Expand Down Expand Up @@ -187,6 +188,7 @@ def _compile(template_name):
"json": json,
"sorted": sorted,
"staff": staff,
"turnstile": turnstile,
"resource_path": get_resource_path,
})

Expand Down
6 changes: 6 additions & 0 deletions weasyl/errorcode.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from weasyl import macro as m


userid = "This user doesn't seem to be in our database."
submitid = "This submission doesn't seem to be in our database."
charid = "This character doesn't seem to be in our database."
Expand Down Expand Up @@ -136,6 +139,9 @@
"titleTooLong": "That title is too long.",
"token": token,
"tooManyPreferenceTags": "You cannot have more than 50 preference tags.",
"turnstileMissing": (
"A required bot check failed. Please go back, refresh the page, and try again.\n\n"
f"If issues persist, contact support at [{m.MACRO_SUPPORT_ADDRESS}](mailto:{m.MACRO_SUPPORT_ADDRESS})."),
"TwoFactorAuthenticationAuthenticationAttemptsExceeded": (
"You have incorrectly entered your 2FA token or recovery code too many times. Please try logging in again."),
"TwoFactorAuthenticationAuthenticationTimeout": "Your authentication session has timed out. Please try logging in again.",
Expand Down
8 changes: 8 additions & 0 deletions weasyl/templates/common/turnstile.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
$def with (action)
$if turnstile.SITE_KEY is not None:
<fieldset>
<legend class="label">Bot Check</legend>

<div class="cf-turnstile" data-sitekey="${turnstile.SITE_KEY}" data-action="${action}" data-size="flexible" style="height: 65px"></div>$# inline height: avoid layout shift
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async></script>
</fieldset>
10 changes: 7 additions & 3 deletions weasyl/templates/etc/forgotpassword.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

<form class="form skinny clear" name="forgotpassword" action="/forgotpassword" method="post">
<label for="fp-email">Email Address</label>
<input type="email" class="input" id="fp-email" name="email" required maxlength="254" style="margin-bottom: 1em" />
<input type="email" class="input" id="fp-email" name="email" required maxlength="254" />

<button type="submit" class="button positive" style="float: right;">Continue</button>
<a href="/" class="button">Return Home</a>
$:{COMPILE("common/turnstile.html")(action="password-reset")}

<div class="form-actions">
<button type="submit" class="button positive" style="float: right;">Continue</button>
<a href="/" class="button">Return Home</a>
</div>
</form>

</div>
2 changes: 2 additions & 0 deletions weasyl/templates/etc/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
</label>
</fieldset>

$:{COMPILE("common/turnstile.html")(action="signup")}

<script type="module" src="${resource_path('js/signup.js')}" async></script>
<script src="${resource_path('js/zxcvbn.js')}" async></script>
<script src="${resource_path('js/zxcvbn-check.js')}" async></script>
Expand Down
6 changes: 6 additions & 0 deletions weasyl/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
macro,
media,
middleware,
turnstile,
)
from weasyl.controllers.routes import setup_routes_and_views
from weasyl.wsgi import make_wsgi_app
Expand Down Expand Up @@ -169,6 +170,11 @@ def _fetch_rates():
monkeypatch.setattr(commishinfo, '_fetch_rates', _fetch_rates)


@pytest.fixture(autouse=True)
def disable_turnstile(monkeypatch):
monkeypatch.setattr(turnstile, "SITE_KEY", None)


@pytest.fixture(scope='session')
def wsgi_app():
return make_wsgi_app(configure_cache=False)
Expand Down
71 changes: 71 additions & 0 deletions weasyl/turnstile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import enum
import logging
import requests

from weasyl.config import config_obj
from weasyl.error import WeasylError


logger = logging.getLogger(__name__)


SITE_KEY = config_obj.get("turnstile", "site_key", fallback=None)

_SECRET_KEY = None if SITE_KEY is None else config_obj.get("turnstile", "secret_key")

ENFORCE = None if SITE_KEY is None else config_obj.getboolean("turnstile", "enforce")
"""
To allow a grace period for forms loaded before Turnstile was served, first deploy with `enforce = false`, then `enforce = true` later.
"""


@enum.unique
class Result(enum.Enum):
NOT_LOADED = enum.auto()
NOT_COMPLETED = enum.auto()
INVALID = enum.auto()
SUCCESS = enum.auto()


def _check(request) -> Result:
turnstile_response = request.POST.get("cf-turnstile-response")

if turnstile_response is None:
return Result.NOT_LOADED

if not turnstile_response:
return Result.NOT_COMPLETED

turnstile_validation = requests.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", data={
"secret": _SECRET_KEY,
"response": turnstile_response,
"remoteip": request.client_addr,
}).json()

if not turnstile_validation["success"]:
error_codes = turnstile_validation["error-codes"]

if not {"invalid-input-response", "timeout-or-duplicate"}.issuperset(error_codes):
logger.warn("Unexpected Turnstile error codes: %r", error_codes) # pragma: no cover

return Result.INVALID

return Result.SUCCESS


def require(request) -> None:
if SITE_KEY is None:
return

result = _check(request)

if result == Result.SUCCESS:
return

if ENFORCE:
raise WeasylError("turnstileMissing")

if result == Result.NOT_LOADED:
logger.info("Form submitted without Turnstile field in non-enforcing mode")
else:
logger.warn("Turnstile validation failed in non-enforcing mode: %s", result)

0 comments on commit fdbe41a

Please sign in to comment.