Skip to content

/accounts/signup/ is open despite ACCOUNT_ALLOW_REGISTRATION = False #186

Description

@alexeygrigorev

Status

Decision required from the product owner before any implementation. This issue stays open
with the decision label until the owner picks one of the options below. Nothing here is a
recommendation, and no implementation should start while the decision is open
(_docs/PROCESS.md, decision label).

Priority P0: whatever the owner decides, the current state is self-contradictory — a settings
flag that reads "registration disabled" while a public form on every page's masthead registers
accounts — and the two readings imply opposite fixes.

Normative context

  • _docs/specs/01-platform-architecture.md § "Member signup, profile, Slack, and course
    registration" specifies email/password signup as conditional: "Email/password signup, when
    enabled, collects only email, its credential, and the required account/privacy
    acknowledgement."
    The enabling condition is defined nowhere.
  • _docs/specs/07-security-privacy-operations.md § "Edge and application abuse controls": the
    per-source-IP WAF rate rule for signup/login/profile/Slack/registration (60 requests per five
    minutes) starts in count mode.
  • Commit 8c69a93 ("Bring the account entrance into the design system") deliberately left
    openness unchanged and deferred it to this issue.

Verified behavior at current main (9603342)

Observed by live request under website.settings.local. The flag is inherited from
website/settings/base.py; no settings module (development, local, local_review,
test, production) overrides it and no environment variable reads it, so it is False
everywhere, dev included.

  1. GET /accounts/signup/ returns 200 and renders the design 5a signup page
    (accounts/templates/account/signup.html, added in 8c69a93): three provider buttons, then
    the email/password card with email, password1, password2, the CSRF token, and next
    passthrough.
  2. POST /accounts/signup/ with a fresh email and matching passwords returns 302 to / with
    the session logged in. It creates an is_active=True CustomUser with
    identity_state="legacy", normalized_email set, and a primary EmailAddress row with
    verified=False. No verification email is ever sent
    (ACCOUNT_EMAIL_VERIFICATION = "none", website/settings/base.py:183). Verified by
    request; the probe user was deleted afterwards.
  3. Entrances: the masthead "Sign up free" button on every design 5a page
    (templates/core/_site_shell_head.html:275) and both homepage CTAs "Create your free
    account" (templates/core/home.html:650,1069) link to {% url 'account_signup' %}, and
    accounts/tests/test_account_entrance_design.py exercises the open form as expected
    behavior.

Why ACCOUNT_ALLOW_REGISTRATION = False does not close it

  • website/settings/base.py:189 sets the flag (and course_management/settings.py:380 repeats
    it under the comment # Disable sign-up form — the copied course-platform intent). That name
    is not a django-allauth setting. The only reader in this repository is
    accounts/identity_inventory.py:213, which reports it as account_registration_enabled
    inventory metadata — so the inventory today claims registration is disabled while the form is
    open.
  • allauth decides local signup openness through ACCOUNT_ADAPTER.is_open_for_signup().
    ACCOUNT_ADAPTER is never set anywhere (only SOCIALACCOUNT_ADAPTER appears,
    website/settings/base.py:190), so allauth's DefaultAccountAdapter applies; verified live:
    it answers is_open_for_signup() → True.
  • The social path is gated independently and fail-closed:
    accounts.auth.ConsolidatingSocialAccountAdapter (accounts/auth.py:258) returns False
    from is_open_for_signup (accounts/auth.py:261-264), and pre_social_login only links a
    provider onto exactly one existing durable account. Social providers cannot register new
    accounts whether the flag is True or False.
    The flag gates nothing at all.

Net state: social signup closed, local signup open — the opposite of what the settings read
like at a glance.

Security, abuse, and SEO implications

Keeping local signup open (today's behavior):

  • Any anonymous visitor or script can create unlimited active user rows with arbitrary
    unverified emails: no captcha, no email verification, and the WAF signup rate rule is still
    in count mode (spec 07).
  • Every row lands in the Django admin user list and emits an auth.user_created observability
    event (accounts/signals.py:55-66), polluting operational views.
  • ACCOUNT_UNIQUE_EMAIL = True (website/settings/base.py:186) means a squatted address
    blocks the real owner's later local signup with that email.
  • Squatted rows also collide with the durable-identity path: _activate_verified_identity
    (accounts/auth.py:211-231) refuses to activate a verified identity while any other user
    holds the same normalized_email (_has_unresolved_email_collision,
    accounts/auth.py:202-210), so a pre-registered row can deny the legitimate owner's social
    login with normalized_email_conflict. (The social adapter's candidate lookup requires a
    verified EmailAddress (accounts/auth.py:191-199), so an unverified squatting row cannot
    by itself capture a social login — the exposure is denial, not takeover.)
  • SEO: negligible direct effect; an open auth form is a normal crawlable page.

Closing local signup behind the flag:

  • Removes every vector above.
  • New members can no longer self-serve an account by any path, because social login is
    fail-closed onto existing accounts. Account creation becomes operator/invite-driven until a
    deliberate path is built. The homepage's primary CTA ("Create your free account") and the
    masthead "Sign up free" button stop matching reality and must be redirected or reworded —
    they are on every page.
  • No design work is needed: the closed state already has a styled page
    (accounts/templates/account/signup_closed.html, from 8c69a93).

Options for the owner

Option A — close local signup (make the flag mean what it says)

Requires:

  • Set ACCOUNT_ADAPTER to an adapter whose is_open_for_signup() returns
    settings.ACCOUNT_ALLOW_REGISTRATION; allauth then routes GET and POST to the already-styled
    signup_closed.html.
  • Decide the masthead "Sign up free" and homepage CTA destinations (e.g. /accounts/login/,
    or keep them pointing at /accounts/signup/ to show the closed notice) and update
    templates/core/_site_shell_head.html and templates/core/home.html.
  • Make accounts/identity_inventory.py report the enforced value (or drop the field) so the
    inventory cannot again claim closed-while-open.
  • Update accounts/tests/test_account_entrance_design.py and related tests; add tests that GET
    shows the closed notice and POST creates no user.

Option B — keep local signup open, deliberately

Requires:

  • Set ACCOUNT_ALLOW_REGISTRATION = True (or delete the flag and make the inventory report the
    adapter's actual answer) so no dead, misleading setting remains.
  • Record the decision in _docs/specs/01-platform-architecture.md § "Member signup, profile,
    Slack, and course registration" (the "when enabled" clause needs its condition defined).
  • Decide and record the abuse posture: keep ACCOUNT_EMAIL_VERIFICATION = "none" or move to
    verification; confirm the WAF signup rate rule promotion plan (spec 07).
  • Treat /accounts/signup/ as a first-class open page in the accessibility registry and core
    browser/smoke coverage (partially exists via test_account_entrance_design.py).

Option C — something else

E.g. invite-only signup, email-verified-only signup, or closing local signup while opening a
deliberate provider signup path. The owner should state the intended behavior in this issue;
the PM will re-groom into implementation criteria at that point.

Non-goals (all options)

  • No changes to the social consolidation adapter or its fail-closed semantics
    (accounts/auth.py).
  • No new signup styling or design work — the open and closed pages are both design 5a already.
  • No password reset, login, MFA, or account-deletion changes.
  • No WAF/Terraform changes in this issue; abuse-control rollout stays owned by spec 07.

Dependencies

  • None technical. This decision blocks any work that assumes a signup posture (growth,
    invitations, cohort onboarding flows).

Acceptance criteria

Apply to whichever option the owner picks. Nothing may be checked before the decision is
recorded in this issue by the owner.

  • The owner has recorded the decision (Option A, B, or C with the intended behavior) in
    this issue.
  • The settings flag and actual signup behavior agree: ACCOUNT_ALLOW_REGISTRATION (or its
    documented replacement) is enforced by code allauth actually calls, or is removed.
  • accounts/identity_inventory.py reports a value matching observed behavior, verified by
    a test.
  • Focused Django tests cover the decided behavior:
    • Option A: GET /accounts/signup/ renders the closed notice; POST /accounts/signup/
      creates no user and no EmailAddress row; the flag forced to True in a test reopens the
      form (proving the gate is real, not hard-coded).
    • Option B: signup POST creates the account per the decided verification posture.
  • Every entrance advertising the old behavior matches the decision: the masthead "Sign up
    free" button (templates/core/_site_shell_head.html) and the homepage CTAs
    (templates/core/home.html) point at destinations consistent with it.
  • _docs/specs/01-platform-architecture.md § "Member signup, profile, Slack, and course
    registration" states the decided signup posture.
  • Browser check at desktop and mobile widths of /accounts/signup/ (and the changed
    masthead/homepage CTA under Option A) under the tester's screenshot gate.

Browser scenarios

  1. A signed-out visitor follows the masthead "Sign up free" button and observes behavior
    consistent with the recorded decision, at desktop and mobile widths.
  2. A signed-out visitor submits the signup form with valid and invalid input and observes the
    decided outcome, with errors announced accessibly wherever the form remains.
  3. An operator reads the identity inventory output and sees a registration value that matches
    what a browser can actually do at /accounts/signup/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must-have or release-blockingauthArea: authbugSomething isn't workingdecisionOwner decision requiredsecurityArea: security

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions