mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	signup: Add optional Altcha to realm registration.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							f434c9d913
						
					
				
				
					commit
					eae18738a6
				
			@@ -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"),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user