Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial refactoring of activation to require POST to activate. #247

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,7 @@ dmypy.json

# Cython debug symbols
cython_debug/

# IDEs.
.idea/
.vscode/
125 changes: 2 additions & 123 deletions docs/activation-workflow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,130 +64,9 @@ specified below), and their context variables, see :ref:`the quick start guide
<default-templates>`.


.. class:: RegistrationView
.. autoclass:: RegistrationView

A subclass of :class:`django_registration.views.RegistrationView`
implementing the signup portion of this workflow.

Important customization points unique to this class are:

.. method:: create_inactive_user(form)

Creates and returns an inactive user account, and calls
:meth:`send_activation_email()` to send the email with the activation
key. The argument ``form`` is a valid registration form instance passed
from :meth:`~django_registration.views.RegistrationView.register()`.

:param django_registration.forms.RegistrationForm form: The registration form.
:rtype: django.contrib.auth.models.AbstractUser

.. method:: get_activation_key(user)

Given an instance of the user model, generates and returns an activation
key (a string) for that user account.

:param django.contrib.auth.models.AbstractUser user: The new user account.
:rtype: str

.. method:: get_email_context(activation_key)

Returns a dictionary of values to be used as template context when
generating the activation email.

:param str activation_key: The activation key for the new user account.
:rtype: dict

.. method:: send_activation_email(user)

Given an inactive user account, generates and sends the activation email
for that account.

:param django.contrib.auth.models.AbstractUser user: The new user account.
:rtype: None

.. attribute:: email_body_template

A string specifying the template to use for the body of the activation
email. Default is ``"django_registration/activation_email_body.txt"``.

.. attribute:: email_subject_template

A string specifying the template to use for the subject of the activation
email. Default is
``"django_registration/activation_email_subject.txt"``. Note that, to avoid
`header-injection vulnerabilities
<https://en.wikipedia.org/wiki/Email_injection>`_, the result of
rendering this template will be forced into a single line of text,
stripping newline characters.

.. class:: ActivationView

A subclass of :class:`django_registration.views.ActivationView` implementing
the activation portion of this workflow.

Errors in activating the user account will raise
:exc:`~django_registration.exceptions.ActivationError`, with one of the
following values for the exception's ``code``:

``"already_activated"``
Indicates the account has already been activated.

``"bad_username"``
Indicates the username decoded from the activation key is invalid (does
not correspond to any user account).

``"expired"``
Indicates the account/activation key has expired.

``"invalid_key"``
Generic indicator that the activation key was invalid.

Important customization points unique to this class are:

.. method:: get_user(username)

Given a username (determined by the activation key), looks up and returns
the corresponding instance of the user model. If no such account exists,
raises :exc:`~django_registration.exceptions.ActivationError` as
described above. In the base implementation, checks the
:attr:`~django.contrib.auth.models.User.is_active` field to avoid
re-activating already-active accounts, and raises
:exc:`~django_registration.exceptions.ActivationError` with code
``already_activated`` to indicate this case.

:param str username: The username of the new user account.
:rtype: django.contrib.auth.models.AbstractUser
:raises django_registration.exceptions.ActivationError: if no
matching inactive user account exists.

.. method:: validate_key(activation_key)

Given the activation key, verifies that it carries a valid signature and
a timestamp no older than the number of days specified in the setting
``ACCOUNT_ACTIVATION_DAYS``, and returns the username from the activation
key. Raises :exc:`~django_registration.exceptions.ActivationError`, as
described above, if the activation key has an invalid signature or if the
timestamp is too old.

:param str activation_key: The activation key for the new user account.
:rtype: str
:raises django_registration.exceptions.ActivationError: if the
activation key has an invalid signature or is expired.

.. note:: **URL patterns for activation**

Although the actual value used in the activation key is the new user
account's username, the URL pattern for :class:`~views.ActivationView`
does not need to match all possible legal characters in a username. The
activation key that will be sent to the user (and thus matched in the
URL) is produced by :func:`django.core.signing.dumps()`, which
base64-encodes its output. Thus, the only characters this pattern needs
to match are those from `the URL-safe base64 alphabet
<http://tools.ietf.org/html/rfc4648#section-5>`_, plus the colon ("``:``")
which is used as a separator.

The default URL pattern for the activation view in
``django_registration.backends.activation.urls`` handles this for you.
.. autoclass:: ActivationView


How it works
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ parsers
paypal
pаypаl
pre
querystring
regex
registrationview
runtime
Expand Down
140 changes: 5 additions & 135 deletions docs/views.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
Base view classes
=================

In order to allow the utmost flexibility in customizing and supporting
different workflows, django-registration makes use of Django's support for
`class-based views
In order to allow flexibility in customizing and supporting different
workflows, django-registration makes use of Django's support for `class-based
views
<https://docs.djangoproject.com/en/stable/topics/class-based-views/>`_. Included
in django-registration are two base classes which can be subclassed to
implement many types of registration workflows.
Expand All @@ -17,137 +17,7 @@ customization points specific to those subclasses. The following reference
covers useful attributes and methods of the base classes, for use in writing
your own custom registration workflows.

.. class:: RegistrationView
.. autoclass:: RegistrationView

A subclass of Django's :class:`~django.views.generic.edit.FormView` which
provides the infrastructure for supporting user registration.

Standard attributes and methods of
:class:`~django.views.generic.edit.FormView` can be overridden to control
behavior as described in Django's documentation, with the exception of
:meth:`get_success_url`, which must use the signature documented below.

When writing your own subclass, one method is required:

.. method:: register(form)

Implement your registration logic here. ``form`` will be the
(already-validated) form filled out by the user during the registration
process (i.e., a valid instance of
:class:`~django_registration.forms.RegistrationForm` or a subclass of
it).

This method should return the newly-registered user instance, and should
send the signal :data:`django_registration.signals.user_registered`. Note
that this is not automatically done for you when writing your own custom
subclass, so you must send this signal manually.

:param django_registration.forms.RegistrationForm form: The registration form to use.
:rtype: django.contrib.auth.models.AbstractUser

Useful optional places to override or customize on subclasses are:

.. attribute:: disallowed_url

The URL to redirect to when registration is disallowed. Can be a
hard-coded string, the string resulting from calling Django's
:func:`~django.urls.reverse` helper, or the lazy object produced by
Django's :func:`~django.urls.reverse_lazy` helper. Default value is the
result of calling :func:`~django.urls.reverse_lazy` with the URL name
``'registration_disallowed'``.

.. attribute:: form_class

The form class to use for user registration. Can be overridden on a
per-request basis (see below). Should be the actual class object; by
default, this class is
:class:`django_registration.forms.RegistrationForm`.

.. attribute:: success_url

The URL to redirect to after successful registration. Can be a hard-coded
string, the string resulting from calling Django's
:func:`~django.urls.reverse` helper, or the lazy object produced by
Django's :func:`~django.urls.reverse_lazy` helper. Can be overridden on a
per-request basis (see below). Default value is :data:`None`; subclasses
must override and provide this.

.. attribute:: template_name

The template to use for user registration. Should be a string. Default
value is ``'django_registration/registration_form.html'``.

.. method:: get_form_class()

Select a form class to use on a per-request basis. If not overridden,
will use :attr:`~form_class`. Should be the actual class object.

:rtype: django_registration.forms.RegistrationForm

.. method:: get_success_url(user)

Return a URL to redirect to after successful registration, on a
per-request or per-user basis. If not overridden, will use
:attr:`~success_url`. Should return a value of the same type as
:attr:`success_url` (see above).

:param django.contrib.auth.models.AbstractUser user: The new user account.
:rtype: str

.. method:: registration_allowed()

Should indicate whether user registration is allowed, either in general
or for this specific request. Default value is the value of the setting
:data:`~django.conf.settings.REGISTRATION_OPEN`.

:rtype: bool


.. class:: ActivationView

A subclass of Django's :class:`~django.views.generic.base.TemplateView`
which provides support for a separate account-activation step, in workflows
which require that.

One method is required:

.. method:: activate(*args, **kwargs)

Implement your activation logic here. You are free to configure your URL
patterns to pass any set of positional or keyword arguments to
:class:`ActivationView`, and they will in turn be passed to this method.

This method should return the newly-activated user instance (if
activation was successful), or raise
:class:`~django_registration.exceptions.ActivationError` (if activation
was not successful).

:rtype: django.contrib.auth.models.AbstractUser
:raises django_registration.exceptions.ActivationError: if activation fails.

Useful places to override or customize on an
:class:`ActivationView` subclass are:

.. attribute:: success_url

The URL to redirect to after successful activation. Can be a hard-coded
string, the string resulting from calling Django's
:func:`~django.urls.reverse` helper, or the lazy object produced by
Django's :func:`~django.urls.reverse_lazy` helper. Can be overridden on a
per-request basis (see below). Default value is :data:`None`; subclasses
must override and provide this.

.. attribute:: template_name

The template to use after failed user activation. Should be a
string. Default value is ``'django_registration/activation_failed.html'``.

.. method:: get_success_url(user)

Return a URL to redirect to after successful activation, on a per-request
or per-user basis. If not overridden, will use
:attr:`~success_url`. Should return a value of the same type as
:attr:`success_url` (see above).

:param django.contrib.auth.models.AbstractUser user: The activated user account.
:rtype: str
.. autoclass:: ActivationView
4 changes: 4 additions & 0 deletions src/django_registration/backends/activation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
https://django-registration.readthedocs.io/

"""

from django.conf import settings

REGISTRATION_SALT = getattr(settings, "REGISTRATION_SALT", "registration")
55 changes: 55 additions & 0 deletions src/django_registration/backends/activation/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Forms used by the two-step activation workflow.

"""

from django import forms
from django.conf import settings
from django.core import signing
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

from . import REGISTRATION_SALT

# pylint: disable=raise-missing-from


class ActivationForm(forms.Form):
"""
Form for the activation step of the two-step activation workflow.

This form has one field, the (string) activation key, which should be an HMAC-signed
value containing the username of the account to activate.

"""

EXPIRED_MESSAGE = _("This account has expired.")
INVALID_KEY_MESSAGE = _("The activation key you provided is invalid.")

activation_key = forms.CharField(widget=forms.HiddenInput())

def clean_activation_key(self):
"""
Validate the signature of the activation key.

"""
activation_key = self.cleaned_data["activation_key"]
try:
username = signing.loads(
activation_key,
salt=REGISTRATION_SALT,
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400,
)
# This is a bit of a hack. Whatever we return here is the value Django will
# insert into cleaned_data under the name of this field, and although
# initially it's the activation-key value we here replace it with the
# username value decoded from that key. This allows the rest of the
# processing chain to avoid the need to decode the activation key again, but
# relies on the fact that we only do this when we've fully verified that the
# activation key was valid -- if it's invalid, cleaned_data will continue to
# have the raw activation key.
return username
except signing.SignatureExpired:
raise ValidationError(self.EXPIRED_MESSAGE, code="expired")
except signing.BadSignature:
raise ValidationError(self.INVALID_KEY_MESSAGE, code="invalid_key")
2 changes: 1 addition & 1 deletion src/django_registration/backends/activation/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
name="django_registration_activation_complete",
),
path(
"activate/<str:activation_key>/",
"activate/",
views.ActivationView.as_view(),
name="django_registration_activate",
),
Expand Down
Loading