authentik.stages.authenticator_totp.tests

Test TOTP API

  1"""Test TOTP API"""
  2
  3from time import time
  4from urllib.parse import parse_qs, urlsplit
  5
  6from django.test.utils import override_settings
  7from django.urls import reverse
  8from rest_framework.test import APITestCase
  9
 10from authentik.core.models import User
 11from authentik.core.tests.utils import create_test_admin_user
 12from authentik.stages.authenticator.tests import TestCase, ThrottlingTestMixin
 13from authentik.stages.authenticator_totp.models import TOTPDevice
 14
 15
 16class AuthenticatorTOTPStage(APITestCase):
 17    """Test TOTP API"""
 18
 19    def test_api_delete(self):
 20        """Test api delete"""
 21        user = User.objects.create(username="foo")
 22        self.client.force_login(user)
 23        dev = TOTPDevice.objects.create(user=user)
 24        response = self.client.delete(
 25            reverse("authentik_api:totpdevice-detail", kwargs={"pk": dev.pk})
 26        )
 27        self.assertEqual(response.status_code, 204)
 28
 29
 30class TOTPDeviceMixin:
 31    """
 32    A TestCase helper that gives us a TOTPDevice to work with.
 33    """
 34
 35    # The next ten tokens
 36    tokens = [
 37        179225,
 38        656163,
 39        839400,
 40        154567,
 41        346912,
 42        471576,
 43        45675,
 44        101397,
 45        491039,
 46        784503,
 47    ]
 48
 49    def setUp(self):
 50        """
 51        Create a device at the fourth time step. The current token is 154567.
 52        """
 53        self.alice = create_test_admin_user("alice", email="alice@example.com")
 54        self.device = self.alice.totpdevice_set.create(
 55            key="2a2bbba1092ffdd25a328ad1a0a5f5d61d7aacc4",
 56            step=30,
 57            t0=int(time() - (30 * 3)),
 58            digits=6,
 59            tolerance=0,
 60            drift=0,
 61        )
 62
 63
 64@override_settings(
 65    OTP_TOTP_SYNC=False,
 66    OTP_TOTP_THROTTLE_FACTOR=0,
 67)
 68class TOTPTest(TOTPDeviceMixin, TestCase):
 69    """TOTP tests"""
 70
 71    def test_default_key(self):
 72        """Ensure default_key is valid"""
 73        device = self.alice.totpdevice_set.create()
 74
 75        # Make sure we can decode the key.
 76        _ = device.bin_key
 77
 78    def test_single(self):
 79        """Test single token"""
 80        results = [self.device.verify_token(token) for token in self.tokens]
 81
 82        self.assertEqual(results, [False] * 3 + [True] + [False] * 6)
 83
 84    def test_tolerance(self):
 85        """Test tolerance"""
 86        self.device.tolerance = 1
 87        results = [self.device.verify_token(token) for token in self.tokens]
 88
 89        self.assertEqual(results, [False] * 2 + [True] * 3 + [False] * 5)
 90
 91    def test_drift(self):
 92        """Test drift"""
 93        self.device.tolerance = 1
 94        self.device.drift = -1
 95        results = [self.device.verify_token(token) for token in self.tokens]
 96
 97        self.assertEqual(results, [False] * 1 + [True] * 3 + [False] * 6)
 98
 99    def test_sync_drift(self):
100        """Test sync drift"""
101        self.device.tolerance = 2
102        with self.settings(OTP_TOTP_SYNC=True):
103            valid = self.device.verify_token(self.tokens[5])
104
105        self.assertTrue(valid)
106        self.assertEqual(self.device.drift, 2)
107
108    def test_no_reuse(self):
109        """Test reuse"""
110        verified1 = self.device.verify_token(self.tokens[3])
111        verified2 = self.device.verify_token(self.tokens[3])
112
113        self.assertEqual(self.device.last_t, 3)
114        self.assertTrue(verified1)
115        self.assertFalse(verified2)
116
117    def test_config_url(self):
118        """Test config_url"""
119        with override_settings(OTP_TOTP_ISSUER=None):
120            url = self.device.config_url
121
122        parsed = urlsplit(url)
123        params = parse_qs(parsed.query)
124
125        self.assertEqual(parsed.scheme, "otpauth")
126        self.assertEqual(parsed.netloc, "totp")
127        self.assertEqual(parsed.path, "/alice")
128        self.assertIn("secret", params)
129        self.assertNotIn("issuer", params)
130
131    def test_config_url_issuer(self):
132        """Test config_url issuer"""
133        with override_settings(OTP_TOTP_ISSUER="example.com"):
134            url = self.device.config_url
135
136        parsed = urlsplit(url)
137        params = parse_qs(parsed.query)
138
139        self.assertEqual(parsed.scheme, "otpauth")
140        self.assertEqual(parsed.netloc, "totp")
141        self.assertEqual(parsed.path, "/example.com%3Aalice")
142        self.assertIn("secret", params)
143        self.assertIn("issuer", params)
144        self.assertEqual(params["issuer"][0], "example.com")
145
146    def test_config_url_issuer_spaces(self):
147        """Test config_url issuer with spaces"""
148        with override_settings(OTP_TOTP_ISSUER="Very Trustworthy Source"):
149            url = self.device.config_url
150
151        parsed = urlsplit(url)
152        params = parse_qs(parsed.query)
153
154        self.assertEqual(parsed.scheme, "otpauth")
155        self.assertEqual(parsed.netloc, "totp")
156        self.assertEqual(parsed.path, "/Very%20Trustworthy%20Source%3Aalice")
157        self.assertIn("secret", params)
158        self.assertIn("issuer", params)
159        self.assertEqual(params["issuer"][0], "Very Trustworthy Source")
160
161    def test_config_url_issuer_method(self):
162        """Test config_url issuer method"""
163        with override_settings(OTP_TOTP_ISSUER=lambda d: d.user.email):
164            url = self.device.config_url
165
166        parsed = urlsplit(url)
167        params = parse_qs(parsed.query)
168
169        self.assertEqual(parsed.scheme, "otpauth")
170        self.assertEqual(parsed.netloc, "totp")
171        self.assertEqual(parsed.path, "/alice%40example.com%3Aalice")
172        self.assertIn("secret", params)
173        self.assertIn("issuer", params)
174        self.assertEqual(params["issuer"][0], "alice@example.com")
175
176    def test_config_url_image(self):
177        """Test config_url with image"""
178        image_url = "https://test.invalid/square.png"
179
180        with override_settings(OTP_TOTP_ISSUER=None, OTP_TOTP_IMAGE=image_url):
181            url = self.device.config_url
182
183        parsed = urlsplit(url)
184        params = parse_qs(parsed.query)
185
186        self.assertEqual(parsed.scheme, "otpauth")
187        self.assertEqual(parsed.netloc, "totp")
188        self.assertEqual(parsed.path, "/alice")
189        self.assertIn("secret", params)
190        self.assertEqual(params["image"][0], image_url)
191
192
193@override_settings(
194    OTP_TOTP_THROTTLE_FACTOR=1,
195)
196class ThrottlingTestCase(TOTPDeviceMixin, ThrottlingTestMixin, TestCase):
197    """Test TOTP Throttling"""
198
199    def valid_token(self):
200        return self.tokens[3]
201
202    def invalid_token(self):
203        return -1
class AuthenticatorTOTPStage(rest_framework.test.APITestCase):
17class AuthenticatorTOTPStage(APITestCase):
18    """Test TOTP API"""
19
20    def test_api_delete(self):
21        """Test api delete"""
22        user = User.objects.create(username="foo")
23        self.client.force_login(user)
24        dev = TOTPDevice.objects.create(user=user)
25        response = self.client.delete(
26            reverse("authentik_api:totpdevice-detail", kwargs={"pk": dev.pk})
27        )
28        self.assertEqual(response.status_code, 204)

Test TOTP API

def test_api_delete(self):
20    def test_api_delete(self):
21        """Test api delete"""
22        user = User.objects.create(username="foo")
23        self.client.force_login(user)
24        dev = TOTPDevice.objects.create(user=user)
25        response = self.client.delete(
26            reverse("authentik_api:totpdevice-detail", kwargs={"pk": dev.pk})
27        )
28        self.assertEqual(response.status_code, 204)

Test api delete

class TOTPDeviceMixin:
31class TOTPDeviceMixin:
32    """
33    A TestCase helper that gives us a TOTPDevice to work with.
34    """
35
36    # The next ten tokens
37    tokens = [
38        179225,
39        656163,
40        839400,
41        154567,
42        346912,
43        471576,
44        45675,
45        101397,
46        491039,
47        784503,
48    ]
49
50    def setUp(self):
51        """
52        Create a device at the fourth time step. The current token is 154567.
53        """
54        self.alice = create_test_admin_user("alice", email="alice@example.com")
55        self.device = self.alice.totpdevice_set.create(
56            key="2a2bbba1092ffdd25a328ad1a0a5f5d61d7aacc4",
57            step=30,
58            t0=int(time() - (30 * 3)),
59            digits=6,
60            tolerance=0,
61            drift=0,
62        )

A TestCase helper that gives us a TOTPDevice to work with.

tokens = [179225, 656163, 839400, 154567, 346912, 471576, 45675, 101397, 491039, 784503]
def setUp(self):
50    def setUp(self):
51        """
52        Create a device at the fourth time step. The current token is 154567.
53        """
54        self.alice = create_test_admin_user("alice", email="alice@example.com")
55        self.device = self.alice.totpdevice_set.create(
56            key="2a2bbba1092ffdd25a328ad1a0a5f5d61d7aacc4",
57            step=30,
58            t0=int(time() - (30 * 3)),
59            digits=6,
60            tolerance=0,
61            drift=0,
62        )

Create a device at the fourth time step. The current token is 154567.

@override_settings(OTP_TOTP_SYNC=False, OTP_TOTP_THROTTLE_FACTOR=0)
class TOTPTest(TOTPDeviceMixin, django.test.testcases.TestCase):
 65@override_settings(
 66    OTP_TOTP_SYNC=False,
 67    OTP_TOTP_THROTTLE_FACTOR=0,
 68)
 69class TOTPTest(TOTPDeviceMixin, TestCase):
 70    """TOTP tests"""
 71
 72    def test_default_key(self):
 73        """Ensure default_key is valid"""
 74        device = self.alice.totpdevice_set.create()
 75
 76        # Make sure we can decode the key.
 77        _ = device.bin_key
 78
 79    def test_single(self):
 80        """Test single token"""
 81        results = [self.device.verify_token(token) for token in self.tokens]
 82
 83        self.assertEqual(results, [False] * 3 + [True] + [False] * 6)
 84
 85    def test_tolerance(self):
 86        """Test tolerance"""
 87        self.device.tolerance = 1
 88        results = [self.device.verify_token(token) for token in self.tokens]
 89
 90        self.assertEqual(results, [False] * 2 + [True] * 3 + [False] * 5)
 91
 92    def test_drift(self):
 93        """Test drift"""
 94        self.device.tolerance = 1
 95        self.device.drift = -1
 96        results = [self.device.verify_token(token) for token in self.tokens]
 97
 98        self.assertEqual(results, [False] * 1 + [True] * 3 + [False] * 6)
 99
100    def test_sync_drift(self):
101        """Test sync drift"""
102        self.device.tolerance = 2
103        with self.settings(OTP_TOTP_SYNC=True):
104            valid = self.device.verify_token(self.tokens[5])
105
106        self.assertTrue(valid)
107        self.assertEqual(self.device.drift, 2)
108
109    def test_no_reuse(self):
110        """Test reuse"""
111        verified1 = self.device.verify_token(self.tokens[3])
112        verified2 = self.device.verify_token(self.tokens[3])
113
114        self.assertEqual(self.device.last_t, 3)
115        self.assertTrue(verified1)
116        self.assertFalse(verified2)
117
118    def test_config_url(self):
119        """Test config_url"""
120        with override_settings(OTP_TOTP_ISSUER=None):
121            url = self.device.config_url
122
123        parsed = urlsplit(url)
124        params = parse_qs(parsed.query)
125
126        self.assertEqual(parsed.scheme, "otpauth")
127        self.assertEqual(parsed.netloc, "totp")
128        self.assertEqual(parsed.path, "/alice")
129        self.assertIn("secret", params)
130        self.assertNotIn("issuer", params)
131
132    def test_config_url_issuer(self):
133        """Test config_url issuer"""
134        with override_settings(OTP_TOTP_ISSUER="example.com"):
135            url = self.device.config_url
136
137        parsed = urlsplit(url)
138        params = parse_qs(parsed.query)
139
140        self.assertEqual(parsed.scheme, "otpauth")
141        self.assertEqual(parsed.netloc, "totp")
142        self.assertEqual(parsed.path, "/example.com%3Aalice")
143        self.assertIn("secret", params)
144        self.assertIn("issuer", params)
145        self.assertEqual(params["issuer"][0], "example.com")
146
147    def test_config_url_issuer_spaces(self):
148        """Test config_url issuer with spaces"""
149        with override_settings(OTP_TOTP_ISSUER="Very Trustworthy Source"):
150            url = self.device.config_url
151
152        parsed = urlsplit(url)
153        params = parse_qs(parsed.query)
154
155        self.assertEqual(parsed.scheme, "otpauth")
156        self.assertEqual(parsed.netloc, "totp")
157        self.assertEqual(parsed.path, "/Very%20Trustworthy%20Source%3Aalice")
158        self.assertIn("secret", params)
159        self.assertIn("issuer", params)
160        self.assertEqual(params["issuer"][0], "Very Trustworthy Source")
161
162    def test_config_url_issuer_method(self):
163        """Test config_url issuer method"""
164        with override_settings(OTP_TOTP_ISSUER=lambda d: d.user.email):
165            url = self.device.config_url
166
167        parsed = urlsplit(url)
168        params = parse_qs(parsed.query)
169
170        self.assertEqual(parsed.scheme, "otpauth")
171        self.assertEqual(parsed.netloc, "totp")
172        self.assertEqual(parsed.path, "/alice%40example.com%3Aalice")
173        self.assertIn("secret", params)
174        self.assertIn("issuer", params)
175        self.assertEqual(params["issuer"][0], "alice@example.com")
176
177    def test_config_url_image(self):
178        """Test config_url with image"""
179        image_url = "https://test.invalid/square.png"
180
181        with override_settings(OTP_TOTP_ISSUER=None, OTP_TOTP_IMAGE=image_url):
182            url = self.device.config_url
183
184        parsed = urlsplit(url)
185        params = parse_qs(parsed.query)
186
187        self.assertEqual(parsed.scheme, "otpauth")
188        self.assertEqual(parsed.netloc, "totp")
189        self.assertEqual(parsed.path, "/alice")
190        self.assertIn("secret", params)
191        self.assertEqual(params["image"][0], image_url)

TOTP tests

def test_default_key(self):
72    def test_default_key(self):
73        """Ensure default_key is valid"""
74        device = self.alice.totpdevice_set.create()
75
76        # Make sure we can decode the key.
77        _ = device.bin_key

Ensure default_key is valid

def test_single(self):
79    def test_single(self):
80        """Test single token"""
81        results = [self.device.verify_token(token) for token in self.tokens]
82
83        self.assertEqual(results, [False] * 3 + [True] + [False] * 6)

Test single token

def test_tolerance(self):
85    def test_tolerance(self):
86        """Test tolerance"""
87        self.device.tolerance = 1
88        results = [self.device.verify_token(token) for token in self.tokens]
89
90        self.assertEqual(results, [False] * 2 + [True] * 3 + [False] * 5)

Test tolerance

def test_drift(self):
92    def test_drift(self):
93        """Test drift"""
94        self.device.tolerance = 1
95        self.device.drift = -1
96        results = [self.device.verify_token(token) for token in self.tokens]
97
98        self.assertEqual(results, [False] * 1 + [True] * 3 + [False] * 6)

Test drift

def test_sync_drift(self):
100    def test_sync_drift(self):
101        """Test sync drift"""
102        self.device.tolerance = 2
103        with self.settings(OTP_TOTP_SYNC=True):
104            valid = self.device.verify_token(self.tokens[5])
105
106        self.assertTrue(valid)
107        self.assertEqual(self.device.drift, 2)

Test sync drift

def test_no_reuse(self):
109    def test_no_reuse(self):
110        """Test reuse"""
111        verified1 = self.device.verify_token(self.tokens[3])
112        verified2 = self.device.verify_token(self.tokens[3])
113
114        self.assertEqual(self.device.last_t, 3)
115        self.assertTrue(verified1)
116        self.assertFalse(verified2)

Test reuse

def test_config_url(self):
118    def test_config_url(self):
119        """Test config_url"""
120        with override_settings(OTP_TOTP_ISSUER=None):
121            url = self.device.config_url
122
123        parsed = urlsplit(url)
124        params = parse_qs(parsed.query)
125
126        self.assertEqual(parsed.scheme, "otpauth")
127        self.assertEqual(parsed.netloc, "totp")
128        self.assertEqual(parsed.path, "/alice")
129        self.assertIn("secret", params)
130        self.assertNotIn("issuer", params)

Test config_url

def test_config_url_issuer(self):
132    def test_config_url_issuer(self):
133        """Test config_url issuer"""
134        with override_settings(OTP_TOTP_ISSUER="example.com"):
135            url = self.device.config_url
136
137        parsed = urlsplit(url)
138        params = parse_qs(parsed.query)
139
140        self.assertEqual(parsed.scheme, "otpauth")
141        self.assertEqual(parsed.netloc, "totp")
142        self.assertEqual(parsed.path, "/example.com%3Aalice")
143        self.assertIn("secret", params)
144        self.assertIn("issuer", params)
145        self.assertEqual(params["issuer"][0], "example.com")

Test config_url issuer

def test_config_url_issuer_spaces(self):
147    def test_config_url_issuer_spaces(self):
148        """Test config_url issuer with spaces"""
149        with override_settings(OTP_TOTP_ISSUER="Very Trustworthy Source"):
150            url = self.device.config_url
151
152        parsed = urlsplit(url)
153        params = parse_qs(parsed.query)
154
155        self.assertEqual(parsed.scheme, "otpauth")
156        self.assertEqual(parsed.netloc, "totp")
157        self.assertEqual(parsed.path, "/Very%20Trustworthy%20Source%3Aalice")
158        self.assertIn("secret", params)
159        self.assertIn("issuer", params)
160        self.assertEqual(params["issuer"][0], "Very Trustworthy Source")

Test config_url issuer with spaces

def test_config_url_issuer_method(self):
162    def test_config_url_issuer_method(self):
163        """Test config_url issuer method"""
164        with override_settings(OTP_TOTP_ISSUER=lambda d: d.user.email):
165            url = self.device.config_url
166
167        parsed = urlsplit(url)
168        params = parse_qs(parsed.query)
169
170        self.assertEqual(parsed.scheme, "otpauth")
171        self.assertEqual(parsed.netloc, "totp")
172        self.assertEqual(parsed.path, "/alice%40example.com%3Aalice")
173        self.assertIn("secret", params)
174        self.assertIn("issuer", params)
175        self.assertEqual(params["issuer"][0], "alice@example.com")

Test config_url issuer method

def test_config_url_image(self):
177    def test_config_url_image(self):
178        """Test config_url with image"""
179        image_url = "https://test.invalid/square.png"
180
181        with override_settings(OTP_TOTP_ISSUER=None, OTP_TOTP_IMAGE=image_url):
182            url = self.device.config_url
183
184        parsed = urlsplit(url)
185        params = parse_qs(parsed.query)
186
187        self.assertEqual(parsed.scheme, "otpauth")
188        self.assertEqual(parsed.netloc, "totp")
189        self.assertEqual(parsed.path, "/alice")
190        self.assertIn("secret", params)
191        self.assertEqual(params["image"][0], image_url)

Test config_url with image

Inherited Members
TOTPDeviceMixin
tokens
setUp
@override_settings(OTP_TOTP_THROTTLE_FACTOR=1)
class ThrottlingTestCase(TOTPDeviceMixin, authentik.stages.authenticator.tests.ThrottlingTestMixin, django.test.testcases.TestCase):
194@override_settings(
195    OTP_TOTP_THROTTLE_FACTOR=1,
196)
197class ThrottlingTestCase(TOTPDeviceMixin, ThrottlingTestMixin, TestCase):
198    """Test TOTP Throttling"""
199
200    def valid_token(self):
201        return self.tokens[3]
202
203    def invalid_token(self):
204        return -1

Test TOTP Throttling

def valid_token(self):
200    def valid_token(self):
201        return self.tokens[3]

Returns a valid token to pass to our device under test.

def invalid_token(self):
203    def invalid_token(self):
204        return -1

Returns an invalid token to pass to our device under test.