Repository: CTFd/CTFd

Issue:
Registration password
Some people have requested the ability for a registration password which I think is a reasonable ask and shouldn't be too hard to implement as a security field. 


Discussion:

Resolving patch:
diff --git a/CTFd/auth.py b/CTFd/auth.py
--- a/CTFd/auth.py
+++ b/CTFd/auth.py
@@ -197,6 +197,7 @@ def register():
         website = request.form.get("website")
         affiliation = request.form.get("affiliation")
         country = request.form.get("country")
+        registration_code = request.form.get("registration_code", "")
 
         name_len = len(name) == 0
         names = Users.query.add_columns("name", "id").filter_by(name=name).first()
@@ -210,6 +211,13 @@ def register():
         valid_email = validators.validate_email(email_address)
         team_name_email_check = validators.validate_email(name)
 
+        if get_config("registration_code"):
+            if (
+                registration_code.lower()
+                != get_config("registration_code", default="").lower()
+            ):
+                errors.append("The registration code you entered was incorrect")
+
         # Process additional user fields
         fields = {}
         for field in UserFields.query.all():
diff --git a/CTFd/forms/auth.py b/CTFd/forms/auth.py
--- a/CTFd/forms/auth.py
+++ b/CTFd/forms/auth.py
@@ -4,7 +4,12 @@
 
 from CTFd.forms import BaseForm
 from CTFd.forms.fields import SubmitField
-from CTFd.forms.users import attach_custom_user_fields, build_custom_user_fields
+from CTFd.forms.users import (
+    attach_custom_user_fields,
+    attach_registration_code_field,
+    build_custom_user_fields,
+    build_registration_code_field,
+)
 
 
 def RegistrationForm(*args, **kwargs):
@@ -18,9 +23,10 @@ class _RegistrationForm(BaseForm):
         def extra(self):
             return build_custom_user_fields(
                 self, include_entries=False, blacklisted_items=()
-            )
+            ) + build_registration_code_field(self)
 
     attach_custom_user_fields(_RegistrationForm)
+    attach_registration_code_field(_RegistrationForm)
 
     return _RegistrationForm(*args, **kwargs)
 
diff --git a/CTFd/forms/users.py b/CTFd/forms/users.py
--- a/CTFd/forms/users.py
+++ b/CTFd/forms/users.py
@@ -2,6 +2,7 @@
 from wtforms.fields.html5 import EmailField
 from wtforms.validators import InputRequired
 
+from CTFd.constants.config import Configs
 from CTFd.forms import BaseForm
 from CTFd.forms.fields import SubmitField
 from CTFd.models import UserFieldEntries, UserFields
@@ -79,6 +80,36 @@ def attach_custom_user_fields(form_cls, **kwargs):
         setattr(form_cls, f"fields[{field.id}]", input_field)
 
 
+def build_registration_code_field(form_cls):
+    """
+    Build the appropriate field so we can render it via the extra property.
+    Add field_type so Jinja knows how to render it.
+    """
+    if Configs.registration_code:
+        field = getattr(form_cls, "registration_code")  # noqa B009
+        field.field_type = "text"
+        return [field]
+    else:
+        return []
+
+
+def attach_registration_code_field(form_cls):
+    """
+    If we have a registration code required, we attach it to the form similar
+    to attach_custom_user_fields
+    """
+    if Configs.registration_code:
+        setattr(  # noqa B010
+            form_cls,
+            "registration_code",
+            StringField(
+                "Registration Code",
+                description="Registration code required to create account",
+                validators=[InputRequired()],
+            ),
+        )
+
+
 class UserSearchForm(BaseForm):
     field = SelectField(
         "Search Field",


Test patch:
diff --git a/tests/users/test_auth.py b/tests/users/test_auth.py
--- a/tests/users/test_auth.py
+++ b/tests/users/test_auth.py
@@ -423,3 +423,44 @@ def test_banned_user():
             r = client.get(route)
             assert r.status_code == 403
     destroy_ctfd(app)
+
+
+def test_registration_code_required():
+    """
+    Test that registration code configuration properly blocks logins
+    with missing and incorrect registration codes
+    """
+    app = create_ctfd()
+    with app.app_context():
+        # Set a registration code
+        set_config("registration_code", "secret-sauce")
+
+        with app.test_client() as client:
+            # Load CSRF nonce
+            r = client.get("/register")
+            resp = r.get_data(as_text=True)
+            assert "Registration Code" in resp
+            with client.session_transaction() as sess:
+                data = {
+                    "name": "user",
+                    "email": "user1@examplectf.com",
+                    "password": "password",
+                    "nonce": sess.get("nonce"),
+                }
+            # Attempt registration without password
+            r = client.post("/register", data=data)
+            resp = r.get_data(as_text=True)
+            assert "The registration code you entered was incorrect" in resp
+
+            # Attempt registration with wrong password
+            data["registration_code"] = "wrong-sauce"
+            r = client.post("/register", data=data)
+            resp = r.get_data(as_text=True)
+            assert "The registration code you entered was incorrect" in resp
+
+            # Attempt registration with right password
+            data["registration_code"] = "secret-sauce"
+            r = client.post("/register", data=data)
+            assert r.status_code == 302
+            assert r.location.startswith("http://localhost/challenges")
+    destroy_ctfd(app)