{% blocktrans %}Our terms of service have changed, so we need you to review the new terms of service and agree to the changes.{% endblocktrans %}
18 | {% else %} 19 |{% blocktrans %}One final thing! We need you to agree to the terms of service to begin using your Sponge account.{% endblocktrans %}
20 | {% endif %} 21 | 34 |{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}
6 | 7 |{% blocktrans %}Your email address has been changed to {{ new_email }}.{% endblocktrans %}
8 | 9 |{% blocktrans %}If you did not intend to make this change, please email staff@spongepowered.org as quickly as possible, as this means that your account has been compromised.{% endblocktrans %}
10 | 11 |{% blocktrans %}Best regards,
12 | The SpongePowered Team{% endblocktrans %}
{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}
6 | 7 |{% blocktrans %}Welcome to SpongePowered! Click the link below to confirm your new email address.{% endblocktrans %}
8 | 9 | 10 | 11 |{% blocktrans %}Best regards,
12 | The SpongePowered Team{% endblocktrans %}
{% blocktrans %}To change the email address associated with your account, please fill out the form below. We'll then send you an email with a link you'll need to click to confirm ownership of the new email address.{% endblocktrans %}
17 | {% crispy form %} 18 |{% blocktrans with email=email %}Got it! An email has been sent to {{ email }} with a link to confirm your email address change, and you should receive it shortly.{% endblocktrans %}
17 |{% trans "Forgot your password? No problem." %}
17 |{% trans "Just enter the email address you signed up with to receive an email with a link to reset your password." %}
18 | {% crispy form %} 19 |{% blocktrans with email=email %}Got it! An email has been sent to {{ email }} with a link to reset your password, and you should receive it shortly.{% endblocktrans %}
17 |{% blocktrans %}Thanks for confirming that you own the username {{ user.username }}.{% endblocktrans %}
17 |{% blocktrans %}Please enter a new password below to reset your password.{% endblocktrans %}
18 | {% crispy form %} 19 |{% blocktrans %}Your email address isn't yet verified, so your account cannot be used.{% endblocktrans %}
19 |{% blocktrans with email=user.email %}We've sent you an email to {{ email }} with a link you need to click to verify your account.{% endblocktrans %}
20 |{% blocktrans %}It may take a minute or two for the email to arrive. If it doesn't show up, click the button below and we'll try to send you another one.{% endblocktrans %}
21 |{% blocktrans with email=user.email change_email_url=change_email_url %}If the email address {{ email }} is not what you intended to sign up with, you can change it here.{% endblocktrans %}
22 |{{ field.help_text|safe }}
6 | {% endif %} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /spongeauth/templates/bootstrap3/layout/help_text_and_errors.html: -------------------------------------------------------------------------------- 1 | {% if help_text_inline and not error_text_inline %} 2 | {% include 'bootstrap3/layout/help_text.html' %} 3 | {% endif %} 4 | 5 | {% if error_text_inline %} 6 | {% include 'bootstrap3/layout/field_errors.html' %} 7 | {% else %} 8 | {% include 'bootstrap3/layout/field_errors_block.html' %} 9 | {% endif %} 10 | 11 | {% if not help_text_inline %} 12 | {% include 'bootstrap3/layout/help_text.html' %} 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /spongeauth/templates/core/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %}{% trans "Sponge Authentication Portal" %}{% endblock %} 4 | {% block main %} 5 |{% blocktrans with username=request.user.username %}You are currently logged in as {{ username }}.{% endblocktrans %}
15 | 16 | 17 | 44 | 45 | 49 |{% trans "You have the following authenticators attached to your account:" %}
16 |{{ device.extra_info }}
22 | {% endif %} 23 |{% trans "You have no authenticators attached to your account. Why not improve your security today?" %}
47 | {% endif %} 48 | {% if can_setup %} 49 | Add new authenticator 50 | {% endif %} 51 |{% trans "Your recovery codes have been regenerated, and the old ones have been removed from your account." %}
16 |{% trans "They are displayed below, and will be displayed only once. They can be used in situations where you don't have access to your usual authenticator." %}
17 |{% for code in codes %}{{ code }}{% if not forloop.last %} 18 | {% endif %}{% endfor %}19 | 22 |
{% trans "Setting up Google Authenticator on your account is easy!" %}
16 |{% trans "Scan the QR code below to add Sponge to your authenticator." %}
19 |
20 |
21 |
22 | ({% trans "or alternatively use your secret" %} {{ b32_secret }}
)
23 |
{% trans "This account has two factor authentication enabled." %}
15 | 16 | {% block twofa %}{% endblock %} 17 |{% trans "Please provide one of your 8 character recovery codes." %}
7 | {% crispy form %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /spongeauth/templates/twofa/verify/totp.html: -------------------------------------------------------------------------------- 1 | {% extends "twofa/verify/base.html" %} 2 | {% load i18n %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block twofa %} 6 |{% trans "Please provide the 6 digit code from your authenticator." %}
7 | {% crispy form %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /spongeauth/twofa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpongePowered/SpongeAuth/6b75e71d15fd76140d970f71e85f776dc69ef432/spongeauth/twofa/__init__.py -------------------------------------------------------------------------------- /spongeauth/twofa/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from . import models 4 | 5 | admin.site.register(models.TOTPDevice) 6 | -------------------------------------------------------------------------------- /spongeauth/twofa/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TwofaConfig(AppConfig): 5 | name = "twofa" 6 | -------------------------------------------------------------------------------- /spongeauth/twofa/forms.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from django import forms 4 | from django.utils.translation import gettext_lazy as _ 5 | from django.utils import timezone 6 | 7 | from crispy_forms.helper import FormHelper 8 | from crispy_forms.layout import Submit, Layout, Field, Hidden 9 | 10 | from . import models 11 | from . import oath 12 | 13 | TOTP_TOLERANCE = 1 14 | 15 | 16 | class TOTPVerifyForm(forms.Form): 17 | response = forms.IntegerField(label=_("Code"), min_value=0, max_value=999_999) 18 | 19 | def __init__(self, *args, **kwargs): 20 | device = kwargs.pop("device") 21 | self.device = device 22 | 23 | signed_secret = kwargs.pop("secret", None) # used for setup flow 24 | self.secret = signed_secret 25 | 26 | super().__init__(*args, **kwargs) 27 | 28 | self.helper = FormHelper() 29 | submit = Submit("submit", _("Log in"), css_class="pull-right") 30 | self.helper.layout = Layout(Field("response", autofocus="autofocus"), submit) 31 | 32 | if signed_secret: 33 | self.helper.layout.append(Hidden("secret", signed_secret)) 34 | submit.value = _("Add authenticator") 35 | 36 | def clean_response(self): 37 | code = self.cleaned_data["response"] 38 | verifier = oath.TOTP(base64.b32decode(self.device.base32_secret), drift=self.device.drift) 39 | # lock verifier to now 40 | verifier.time = verifier.time 41 | last_t = self.device.last_t or -1 42 | ok = verifier.verify(code, tolerance=TOTP_TOLERANCE, min_t=last_t + 1) 43 | if not ok: 44 | raise forms.ValidationError(_("That code could not be verified.")) 45 | 46 | # persist data 47 | self.device.last_t = verifier.t() 48 | self.device.drift = verifier.drift 49 | self.device.last_used_at = timezone.now() 50 | self.device.save() 51 | 52 | return code 53 | 54 | 55 | class PaperVerifyForm(forms.Form): 56 | response = forms.CharField(label=_("Recovery code"), max_length=8, min_length=8) 57 | 58 | def __init__(self, *args, **kwargs): 59 | device = kwargs.pop("device") 60 | self.device = device 61 | 62 | super().__init__(*args, **kwargs) 63 | 64 | self.helper = FormHelper() 65 | submit = Submit("submit", _("Log in"), css_class="pull-right") 66 | self.helper.layout = Layout(Field("response", autofocus="autofocus"), submit) 67 | 68 | def clean_response(self): 69 | code = self.cleaned_data["response"] 70 | try: 71 | code_obj = self.device.codes.get(code=code) 72 | except models.PaperCode.DoesNotExist: 73 | raise forms.ValidationError(_("That code is incorrect.")) 74 | 75 | if code_obj.used_at: 76 | raise forms.ValidationError(_("That code has already been used.")) 77 | 78 | # mark as used 79 | code_obj.used_at = timezone.now() 80 | code_obj.save() 81 | self.device.last_used_at = timezone.now() 82 | self.device.save() 83 | 84 | return code 85 | -------------------------------------------------------------------------------- /spongeauth/twofa/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-01-10 00:05 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Device", 19 | fields=[ 20 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 21 | ("added_at", models.DateTimeField(auto_now_add=True)), 22 | ("last_used_at", models.DateTimeField(blank=True, null=True)), 23 | ("activated_at", models.DateTimeField(blank=True, null=True)), 24 | ("deleted_at", models.DateTimeField(blank=True, null=True)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name="PaperCode", 29 | fields=[ 30 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 31 | ("code", models.CharField(max_length=8)), 32 | ("used_at", models.DateTimeField(blank=True, null=True)), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name="PaperDevice", 37 | fields=[ 38 | ( 39 | "device_ptr", 40 | models.OneToOneField( 41 | auto_created=True, 42 | on_delete=django.db.models.deletion.CASCADE, 43 | parent_link=True, 44 | primary_key=True, 45 | serialize=False, 46 | to="twofa.Device", 47 | ), 48 | ) 49 | ], 50 | bases=("twofa.device",), 51 | ), 52 | migrations.CreateModel( 53 | name="TOTPDevice", 54 | fields=[ 55 | ( 56 | "device_ptr", 57 | models.OneToOneField( 58 | auto_created=True, 59 | on_delete=django.db.models.deletion.CASCADE, 60 | parent_link=True, 61 | primary_key=True, 62 | serialize=False, 63 | to="twofa.Device", 64 | ), 65 | ), 66 | ("base32_secret", models.CharField(max_length=32)), 67 | ("last_t", models.PositiveIntegerField()), 68 | ("drift", models.IntegerField(default=0)), 69 | ], 70 | bases=("twofa.device",), 71 | ), 72 | migrations.AddField( 73 | model_name="device", 74 | name="owner", 75 | field=models.ForeignKey( 76 | on_delete=django.db.models.deletion.CASCADE, 77 | related_name="twofa_totp_devices", 78 | to=settings.AUTH_USER_MODEL, 79 | ), 80 | ), 81 | migrations.AddField( 82 | model_name="papercode", 83 | name="device", 84 | field=models.ForeignKey( 85 | on_delete=django.db.models.deletion.CASCADE, related_name="codes", to="twofa.PaperDevice" 86 | ), 87 | ), 88 | ] 89 | -------------------------------------------------------------------------------- /spongeauth/twofa/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpongePowered/SpongeAuth/6b75e71d15fd76140d970f71e85f776dc69ef432/spongeauth/twofa/migrations/__init__.py -------------------------------------------------------------------------------- /spongeauth/twofa/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpongePowered/SpongeAuth/6b75e71d15fd76140d970f71e85f776dc69ef432/spongeauth/twofa/tests/__init__.py -------------------------------------------------------------------------------- /spongeauth/twofa/tests/test_oath.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .. import oath 4 | 5 | 6 | def test_hotp_rfc4226(): 7 | key = b"12345678901234567890" 8 | expect_vals = [755_224, 287_082, 359_152, 969_429, 338_314, 254_676, 287_922, 162_583, 399_871, 520_489] 9 | for n, expect in enumerate(expect_vals): 10 | got = oath.hotp(key, n) 11 | assert got == expect 12 | 13 | 14 | def test_totp_zero_behaviour(): 15 | key = b"12345678901234567890" 16 | totp = oath.TOTP(key) 17 | totp.time = 0 18 | assert totp.t() == 0 19 | assert totp.token() == 755_224 # same as index 0 for hotp 20 | assert totp.verify(755_224) 21 | assert not totp.verify(287_082) 22 | 23 | 24 | def test_totp_step_size(): 25 | key = b"12345678901234567890" 26 | totp = oath.TOTP(key) 27 | totp.time = 30 28 | assert totp.t() == 1 29 | 30 | totp = oath.TOTP(key, step=60) 31 | totp.time = 30 32 | assert totp.t() == 0 33 | 34 | 35 | def test_totp_verify_uses_drift(): 36 | key = b"12345678901234567890" 37 | totp = oath.TOTP(key) 38 | totp.time = 30 39 | assert totp.verify(287_082) 40 | assert totp.drift == 0 41 | assert not totp.verify(359_152) 42 | assert totp.verify(359_152, tolerance=1) 43 | assert totp.drift == 1 44 | assert totp.verify(969_429, tolerance=1) 45 | assert totp.drift == 2 46 | assert not totp.verify(287_082, tolerance=1) 47 | 48 | 49 | def test_totp_verify_respects_min_t(): 50 | key = b"12345678901234567890" 51 | totp = oath.TOTP(key) 52 | totp.time = 30 53 | assert totp.verify(287_082) 54 | assert totp.verify(287_082, min_t=1) 55 | assert not totp.verify(287_082, min_t=2) 56 | 57 | 58 | def test_totp_uses_t0(): 59 | key = b"12345678901234567890" 60 | totp = oath.TOTP(key) 61 | totp.t0 = time.time() - 1 62 | assert totp.t() == 0 63 | 64 | 65 | def test_totp_time(): 66 | key = b"12345678901234567890" 67 | totp = oath.TOTP(key) 68 | totp.time = 13131 69 | assert totp.time == 13131 70 | del totp.time 71 | assert totp.time != 13131 72 | -------------------------------------------------------------------------------- /spongeauth/twofa/tests/test_view_list.py: -------------------------------------------------------------------------------- 1 | import django.test 2 | from django.utils import timezone 3 | import django.core.signing 4 | 5 | import pytest 6 | 7 | import accounts.models 8 | from .. import models 9 | 10 | 11 | @pytest.mark.django_db 12 | class TestList(django.test.TestCase): 13 | def setUp(self): 14 | self.user = accounts.models.User.objects.create_user( 15 | username="fred", email="fred@secret.com", password="secret", email_verified=True, twofa_enabled=True 16 | ) 17 | self.user._test_agree_all_tos() 18 | self.other_user = accounts.models.User.objects.create_user( 19 | username="bob", email="bob@secret.com", password="secret", email_verified=True, twofa_enabled=True 20 | ) 21 | self.other_user._test_agree_all_tos() 22 | 23 | self.dead_backup_device = models.PaperDevice( 24 | owner=self.user, activated_at=timezone.now(), deleted_at=timezone.now() 25 | ) 26 | self.dead_backup_device.save() 27 | 28 | self.backup_device = models.PaperDevice(owner=self.user, activated_at=timezone.now()) 29 | self.backup_device.save() 30 | 31 | self.totp_device = models.TOTPDevice(owner=self.user, activated_at=timezone.now(), last_t=0) 32 | self.totp_device.save() 33 | 34 | self.bobs_totp_device = models.TOTPDevice(owner=self.other_user, activated_at=timezone.now(), last_t=0) 35 | self.bobs_totp_device.save() 36 | 37 | self.client = django.test.Client() 38 | self.login(self.client) 39 | 40 | def login(self, c, username="fred"): 41 | assert c.login(username=username, password="secret") 42 | 43 | def path(self): 44 | return django.shortcuts.reverse("twofa:list") 45 | 46 | def test_requires_login(self): 47 | client = django.test.Client() 48 | resp = client.get(self.path()) 49 | assert resp.status_code == 302 50 | 51 | def test_lists(self): 52 | resp = self.client.get(self.path()) 53 | assert resp.status_code == 200 54 | assert set(resp.context[-1]["devices"]) == {self.totp_device, self.backup_device} 55 | -------------------------------------------------------------------------------- /spongeauth/twofa/tests/test_view_regenerate.py: -------------------------------------------------------------------------------- 1 | import django.test 2 | from django.utils import timezone 3 | import django.core.signing 4 | 5 | import pytest 6 | 7 | import accounts.models 8 | from .. import models 9 | 10 | 11 | @pytest.mark.django_db 12 | class TestRegenerate(django.test.TestCase): 13 | def setUp(self): 14 | self.user = accounts.models.User.objects.create_user( 15 | username="fred", email="fred@secret.com", password="secret", email_verified=True, twofa_enabled=True 16 | ) 17 | self.user._test_agree_all_tos() 18 | 19 | self.dead_backup_device = models.PaperDevice( 20 | owner=self.user, activated_at=timezone.now(), deleted_at=timezone.now() 21 | ) 22 | self.dead_backup_device.save() 23 | 24 | self.backup_device = models.PaperDevice(owner=self.user, activated_at=timezone.now()) 25 | self.backup_device.save() 26 | 27 | self.totp_device = models.TOTPDevice(owner=self.user, activated_at=timezone.now(), last_t=0) 28 | self.totp_device.save() 29 | 30 | self.client = django.test.Client() 31 | self.login(self.client) 32 | 33 | def login(self, c, username="fred"): 34 | assert c.login(username=username, password="secret") 35 | 36 | def path(self, device_id=None, device=None): 37 | return django.shortcuts.reverse("twofa:regenerate", kwargs={"device_id": device_id or device.id}) 38 | 39 | def test_requires_login(self): 40 | client = django.test.Client() 41 | resp = client.get(self.path(device_id=1)) 42 | assert resp.status_code == 302 43 | 44 | def test_rejects_get(self): 45 | resp = self.client.get(self.path(device=self.backup_device)) 46 | assert resp.status_code == 405 47 | 48 | def test_regenerate_someone_elses_device(self): 49 | bob = accounts.models.User.objects.create_user( 50 | username="bob", email="bob@secret.com", password="secret", email_verified=True 51 | ) 52 | bob._test_agree_all_tos() 53 | self.login(self.client, username="bob") 54 | 55 | resp = self.client.post(self.path(device=self.backup_device)) 56 | assert resp.status_code == 404 57 | 58 | def test_regenerate_deleted_device(self): 59 | resp = self.client.post(self.path(device=self.dead_backup_device)) 60 | assert resp.status_code == 404 61 | 62 | def test_regenerate_unregeneratable_device(self): 63 | resp = self.client.post(self.path(device=self.totp_device)) 64 | assert resp.status_code == 302 65 | 66 | def test_happy_path(self): 67 | assert not models.PaperCode.objects.exists() 68 | resp = self.client.post(self.path(device=self.backup_device)) 69 | assert resp.status_code == 302 70 | assert models.PaperCode.objects.exists() 71 | -------------------------------------------------------------------------------- /spongeauth/twofa/tests/test_view_remove.py: -------------------------------------------------------------------------------- 1 | import django.test 2 | from django.utils import timezone 3 | import django.core.signing 4 | 5 | import pytest 6 | 7 | import accounts.models 8 | from .. import models 9 | 10 | 11 | @pytest.mark.django_db 12 | class TestRemove(django.test.TestCase): 13 | def setUp(self): 14 | self.user = accounts.models.User.objects.create_user( 15 | username="fred", email="fred@secret.com", password="secret", email_verified=True, twofa_enabled=True 16 | ) 17 | self.user._test_agree_all_tos() 18 | 19 | self.dead_totp_device = models.TOTPDevice( 20 | owner=self.user, last_t=0, activated_at=timezone.now(), deleted_at=timezone.now() 21 | ) 22 | self.dead_totp_device.save() 23 | 24 | self.totp_device = models.TOTPDevice(owner=self.user, last_t=0, activated_at=timezone.now()) 25 | self.totp_device.save() 26 | 27 | self.backup_device = models.PaperDevice(owner=self.user, activated_at=timezone.now()) 28 | self.backup_device.save() 29 | 30 | self.client = django.test.Client() 31 | self.login(self.client) 32 | 33 | def login(self, c, username="fred"): 34 | assert c.login(username=username, password="secret") 35 | 36 | def path(self, device=None, device_id=None): 37 | return django.shortcuts.reverse("twofa:remove", kwargs={"device_id": device_id or device.id}) 38 | 39 | def test_requires_login(self): 40 | client = django.test.Client() 41 | resp = client.get(self.path(device_id=1)) 42 | assert resp.status_code == 302 43 | 44 | def test_rejects_get(self): 45 | resp = self.client.get(self.path(device=self.totp_device)) 46 | assert resp.status_code == 405 47 | 48 | def test_remove_someone_elses_device(self): 49 | bob = accounts.models.User.objects.create_user( 50 | username="bob", email="bob@secret.com", password="secret", email_verified=True 51 | ) 52 | bob._test_agree_all_tos() 53 | self.login(self.client, username="bob") 54 | 55 | resp = self.client.post(self.path(device=self.totp_device)) 56 | assert resp.status_code == 404 57 | 58 | def test_remove_deleted_device(self): 59 | resp = self.client.post(self.path(device=self.dead_totp_device)) 60 | assert resp.status_code == 404 61 | 62 | def test_remove_undeletable_device(self): 63 | resp = self.client.post(self.path(device=self.backup_device)) 64 | assert resp.status_code == 302 65 | 66 | def test_remove_last_remaining(self): 67 | resp = self.client.post(self.path(device=self.totp_device)) 68 | assert resp.status_code == 302 69 | totp_device = models.TOTPDevice.objects.get(id=self.totp_device.id) 70 | assert totp_device.deleted_at is not None 71 | user = accounts.models.User.objects.get(id=self.user.id) 72 | assert not user.twofa_enabled 73 | 74 | def test_remove_with_remaining(self): 75 | self.dead_totp_device.deleted_at = None 76 | self.dead_totp_device.save() 77 | 78 | resp = self.client.post(self.path(device=self.totp_device)) 79 | assert resp.status_code == 302 80 | totp_device = models.TOTPDevice.objects.get(id=self.totp_device.id) 81 | assert totp_device.deleted_at is not None 82 | user = accounts.models.User.objects.get(id=self.user.id) 83 | assert user.twofa_enabled 84 | -------------------------------------------------------------------------------- /spongeauth/twofa/tests/test_view_setup_backup.py: -------------------------------------------------------------------------------- 1 | import django.test 2 | from django.utils import timezone 3 | 4 | import pytest 5 | 6 | import accounts.models 7 | from .. import models 8 | 9 | 10 | @pytest.mark.django_db 11 | class TestSetupBackup(django.test.TestCase): 12 | def setUp(self): 13 | self.user = accounts.models.User.objects.create_user( 14 | username="fred", email="fred@secret.com", password="secret", email_verified=True 15 | ) 16 | self.user._test_agree_all_tos() 17 | 18 | def login(self, c): 19 | assert c.login(username="fred", password="secret") 20 | 21 | def path(self, device=None, device_id=None): 22 | return django.shortcuts.reverse("twofa:paper-code", kwargs={"device_id": device_id or device.id}) 23 | 24 | def test_requires_login(self): 25 | client = django.test.Client() 26 | resp = client.get(self.path(device_id=1)) 27 | assert resp.status_code == 302 28 | 29 | def test_only_own_devices(self): 30 | other_user = accounts.models.User() 31 | other_user.save() 32 | other_device = models.PaperDevice(owner=other_user) 33 | other_device.save() 34 | 35 | client = django.test.Client() 36 | self.login(client) 37 | resp = client.get(self.path(device_id=other_device.pk)) 38 | assert resp.status_code == 404 39 | 40 | def test_forbids_active_device(self): 41 | active_device = models.PaperDevice(owner=self.user, activated_at=timezone.now()) 42 | active_device.save() 43 | 44 | client = django.test.Client() 45 | self.login(client) 46 | resp = client.get(self.path(device_id=active_device.pk)) 47 | assert resp.status_code == 404 48 | 49 | def test_forbids_deleted_device(self): 50 | deleted_device = models.PaperDevice(owner=self.user, deleted_at=timezone.now()) 51 | deleted_device.save() 52 | 53 | client = django.test.Client() 54 | self.login(client) 55 | resp = client.get(self.path(device_id=deleted_device.pk)) 56 | assert resp.status_code == 404 57 | 58 | def test_renders_codes(self): 59 | device = models.PaperDevice(owner=self.user) 60 | device.save() 61 | 62 | models.PaperCode(device=device, code="12345678").save() 63 | models.PaperCode(device=device, code="1337beef").save() 64 | 65 | client = django.test.Client() 66 | self.login(client) 67 | resp = client.get(self.path(device_id=device.pk)) 68 | assert resp.status_code == 200 69 | assert set(resp.context[-1]["codes"]) == {"12345678", "1337beef"} 70 | 71 | assert device not in models.PaperDevice.objects.active_for_user(self.user) 72 | 73 | def test_activates_on_post(self): 74 | device = models.PaperDevice(owner=self.user) 75 | device.save() 76 | 77 | client = django.test.Client() 78 | self.login(client) 79 | resp = client.post(self.path(device_id=device.pk)) 80 | assert resp.status_code == 302 81 | 82 | assert device in models.PaperDevice.objects.active_for_user(self.user) 83 | 84 | def test_redirects_on_post_to_next(self): 85 | device = models.PaperDevice(owner=self.user) 86 | device.save() 87 | 88 | client = django.test.Client() 89 | self.login(client) 90 | resp = client.post(self.path(device_id=device.pk) + "?next=/aardvark") 91 | assert resp.status_code == 302 92 | assert resp["Location"] == "/aardvark" 93 | 94 | def test_redirects_on_post_without_next(self): 95 | device = models.PaperDevice(owner=self.user) 96 | device.save() 97 | 98 | client = django.test.Client() 99 | self.login(client) 100 | resp = client.post(self.path(device_id=device.pk)) 101 | assert resp.status_code == 302 102 | assert resp["Location"] == django.shortcuts.reverse("twofa:list") 103 | -------------------------------------------------------------------------------- /spongeauth/twofa/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | import twofa.views 4 | 5 | app_name = "twofa" 6 | 7 | urlpatterns = [ 8 | re_path(r"^verify/$", twofa.views.verify, name="verify"), 9 | re_path(r"^verify/(?P