signup: Add optional Altcha to realm registration.

This commit is contained in:
Alex Vandiver
2025-04-16 16:28:08 +00:00
committed by Tim Abbott
parent f434c9d913
commit eae18738a6
16 changed files with 330 additions and 4 deletions

View File

@@ -1,9 +1,12 @@
import base64
import logging
import re
from email.headerregistry import Address
from typing import Any
import dns.resolver
import orjson
from altcha import verify_solution
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate, password_validation
@@ -11,7 +14,10 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, Set
from django.contrib.auth.tokens import PasswordResetTokenGenerator, default_token_generator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.forms.renderers import BaseRenderer
from django.http import HttpRequest
from django.utils.html import format_html
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from markupsafe import Markup
@@ -333,6 +339,85 @@ class RealmCreationForm(RealmDetailsForm):
super().__init__(*args, **kwargs)
class AltchaWidget(forms.TextInput):
@override
def render(
self,
name: str,
value: Any,
attrs: dict[str, Any] | None = None,
renderer: BaseRenderer | None = None,
) -> SafeString:
return format_html(
(
"<altcha-widget"
' name="captcha"'
' challengeurl="/json/antispam_challenge"'
" hidelogo"
" hidefooter"
' floating="bottom"'
" refetchonexpire"
' style="{}"'
' strings="{}"'
">"
),
"--altcha-max-width: 300px;",
orjson.dumps(
{
"verified": _("Verified that you're a human user!"),
"verifying": _("Verifying that you're not a bot..."),
}
).decode(),
)
class CaptchaRealmCreationForm(RealmCreationForm):
captcha = forms.CharField(required=True, widget=AltchaWidget)
def __init__(
self,
*,
request: HttpRequest,
data: dict[str, Any] | None = None,
initial: dict[str, Any] | None = None,
) -> None:
super().__init__(data=data, initial=initial)
self.request = request
@override
def clean(self) -> None:
if not self.data.get("captcha"):
self.add_error("captcha", _("Validation failed, please try again."))
def clean_captcha(self) -> str:
payload = self.data.get("captcha", "")
try:
ok, err = verify_solution(payload, settings.ALTCHA_HMAC_KEY, check_expires=True)
if not ok:
logging.warning("Invalid altcha solution: %s", err)
raise forms.ValidationError(_("Validation failed, please try again."))
except forms.ValidationError:
raise
except Exception as e:
logging.exception(e)
raise forms.ValidationError(_("Validation failed, please try again."))
payload = orjson.loads(base64.b64decode(payload))
challenge = payload["challenge"]
session_challenges = [e[0] for e in self.request.session.get("altcha_challenges", [])]
if challenge not in session_challenges:
logging.warning("Expired or replayed altcha solution")
raise forms.ValidationError(_("Validation failed, please try again."))
# Remove the successful solve from the session, to prevent replay
self.request.session["altcha_challenges"] = [
e for e in self.request.session.get("altcha_challenges", []) if e[0] != challenge
]
return payload
class LoggingSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField(
label=_("New password"),