Skip to content

Commit

Permalink
Prepare for 0.12.0 (#204)
Browse files Browse the repository at this point in the history
* Add migration management command from pennersr/django-allauth#3420

* Add minimal smoke test for management command

* Bump version

* Update changelog

* Update readme

---------

Co-authored-by: Raymond Penners <raymond.penners@intenct.nl>
  • Loading branch information
akx and pennersr authored Jan 16, 2025
1 parent 006e5f7 commit 99737c0
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 15 deletions.
28 changes: 23 additions & 5 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,36 @@
Changelog
#########

0.12.0 - Unreleased
===================
0.12.0 - January 2025
=====================

Note
----

This may be the last version of django-allauth-2fa under this stewardship;
allauth contains its own ``allauth.mfa`` package that should be used instead
for versions of Allauth >= 0.58.0. The dependency range for ``allauth`` in this
version has been updated accordingly; this release will conflict with newer versions
of ``allauth`` on purpose.

See https://github.com/valohai/django-allauth-2fa/issues/189 for discussion.

You can use the experimental ``allauth_2fa_migrate`` management command to create
``allauth.mfa`` Authenticator objects from your ``allauth_2fa`` data before switching
your production environment over to ``allauth.mfa``.

Possibly breaking changes
-------------------------

* You can't write to `allauth_2fa.app_settings` variables anymore;
instead modify the underlying `django.conf.settings` settings.

New features
------------
* Add flag to make the required entry of an otp code for device removal optional (#169)
New features and fixes
----------------------

* Add flag to make the required entry of an OTP code for device removal optional (#169)
* Add setting to allow generating a different number of backup tokens (#192)
* Potential bugfix for ``redirect_field_name`` AttributeError (#196)

0.11.1 - July 13, 2023
======================
Expand Down
27 changes: 21 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ Welcome to django-allauth-2fa!
.. image:: https://readthedocs.org/projects/django-allauth-2fa/badge/?version=latest
:target: https://django-allauth-2fa.readthedocs.io/

django-allauth-2fa adds `two-factor authentication`_ to `django-allauth`_.
django-allauth-2fa adds `two-factor authentication`_ to
versions of `django-allauth`_ older than 0.58.0.

For newer versions, you should use django-allauth's `built-in MFA support`_.
Please see `issue #189`_ for more information.

django-allauth is a set of `Django`_ applications which help with
authentication, registration, and other account management tasks.

Expand All @@ -22,6 +27,8 @@ Documentation
.. _two-factor authentication: https://en.wikipedia.org/wiki/Multi-factor_authentication
.. _django-allauth: https://github.com/pennersr/django-allauth
.. _Django: https://www.djangoproject.com/
.. _built-in MFA support: https://docs.allauth.org/en/latest/mfa/introduction.html
.. _issue #189: https://github.com/valohai/django-allauth-2fa/issues/189

Features
--------
Expand All @@ -33,6 +40,12 @@ Features
Compatibility
-------------

django-allauth-2fa is _not_ compatible with django-allauth versions newer than
0.58.0.

django-allauth has a built-in MFA implementation since version 0.56.0,
which is likely preferable to this one.

django-allauth-2fa attempts to maintain compatibility with supported versions of
Django, django-allauth, and django-otp.

Expand All @@ -41,18 +54,20 @@ Current versions supported together is:
======== ============== ============== ========================
Django django-allauth django-otp Python
======== ============== ============== ========================
4.1 0.53.0 1.2 3.8, 3.9, 3.10, 3.11
4.2 0.53.0 1.2 3.8, 3.9, 3.10, 3.11
4.1 0.57.2 1.2 3.8, 3.9, 3.10, 3.11
4.2 0.57.2 1.2 3.8, 3.9, 3.10, 3.11
======== ============== ============== ========================

Contributing
------------

django-allauth-2fa was initially created by
`Víðir Valberg Guðmundsson (@valberg)`_, was maintained by
`Percipient Networks`_ for many years, and is now maintained by
`Valohai`_. Please feel free to contribute if you find
django-allauth-2fa useful!
`Percipient Networks`_ for many years, and finally by
`Valohai`_.

Please feel free to contribute if you find django-allauth-2fa useful,
but do note that you should likely be using allauth.mfa instead.

#. Check for open issues or open a fresh issue to start a discussion
around a feature idea or a bug.
Expand Down
2 changes: 1 addition & 1 deletion allauth_2fa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations

__version__ = "0.11.0"
__version__ = "0.12.0"
Empty file.
Empty file.
40 changes: 40 additions & 0 deletions allauth_2fa/management/commands/allauth_2fa_migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

import base64

from allauth.mfa.adapter import get_adapter
from allauth.mfa.models import Authenticator
from django.core.management.base import BaseCommand
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice


class Command(BaseCommand):
def handle(self, **options):
adapter = get_adapter()
authenticators = []
for totp in TOTPDevice.objects.filter(confirmed=True).iterator():
recovery_codes = set()
for sdevice in StaticDevice.objects.filter(
confirmed=True,
user_id=totp.user_id,
).iterator():
recovery_codes.update(sdevice.token_set.values_list("token", flat=True))
secret = base64.b32encode(bytes.fromhex(totp.key)).decode("ascii")
totp_authenticator = Authenticator(
user_id=totp.user_id,
type=Authenticator.Type.TOTP,
data={"secret": adapter.encrypt(secret)},
)
authenticators.append(totp_authenticator)
authenticators.append(
Authenticator(
user_id=totp.user_id,
type=Authenticator.Type.RECOVERY_CODES,
data={
"migrated_codes": [adapter.encrypt(c) for c in recovery_codes],
},
),
)
Authenticator.objects.bulk_create(authenticators)
self.stdout.write(f"Created {len(authenticators)} Authenticators")
1 change: 1 addition & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
# Enable allauth.
"allauth",
"allauth.account",
"allauth.mfa", # For testing the migration.
# Required to render the default template for 'account_login'.
"allauth.socialaccount",
# Configure the django-otp package.
Expand Down
24 changes: 21 additions & 3 deletions tests/test_allauth_2fa.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.core.management import call_command
from django.forms import BaseForm
from django.test import override_settings
from django.urls import reverse
Expand Down Expand Up @@ -48,6 +49,13 @@ def pytest_generate_tests(metafunc):
metafunc.parametrize("adapter", ADAPTER_CLASSES, indirect=True)


def create_totp_and_static(user: AbstractUser) -> tuple[TOTPDevice, StaticDevice]:
totp_model = user.totpdevice_set.create()
static_model = user.staticdevice_set.create()
static_model.token_set.create(token=StaticToken.random_token())
return totp_model, static_model


@pytest.fixture(autouse=True)
def adapter(request, settings):
settings.ACCOUNT_ADAPTER = request.param
Expand All @@ -65,9 +73,7 @@ def john() -> AbstractUser:

@pytest.fixture()
def john_with_totp(john: AbstractUser) -> tuple[AbstractUser, TOTPDevice, StaticDevice]:
totp_model = john.totpdevice_set.create()
static_model = john.staticdevice_set.create()
static_model.token_set.create(token=StaticToken.random_token())
totp_model, static_model = create_totp_and_static(john)
return john, totp_model, static_model


Expand Down Expand Up @@ -404,3 +410,15 @@ def test_view_missing_attribute(request, view_cls) -> None:

# Ensure the function doesn't fail when the attribute is missing.
assert OTPAdapter().get_2fa_authenticate_url(request) is not None


def test_migration_management_command():
from allauth.mfa.models import Authenticator

for x in range(10):
user = get_user_model().objects.create(username=f"user{x}")
create_totp_and_static(user)
call_command("allauth_2fa_migrate")
auth_qs = Authenticator.objects
assert auth_qs.filter(type=Authenticator.Type.RECOVERY_CODES).count() == 10
assert auth_qs.filter(type=Authenticator.Type.TOTP).count() == 10

0 comments on commit 99737c0

Please sign in to comment.