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

TOTP tests

def setUp(self):
71    def setUp(self):
72        super().setUp()
73        self.device.set_throttle_factor(0)

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

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

Ensure default_key is valid

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

Test single token

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

Test tolerance

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

Test drift

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

Test sync drift

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

Test reuse

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

Test config_url

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

Test config_url issuer

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

Test config_url issuer with spaces

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

Test config_url issuer method

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

Test config_url with image

Inherited Members
TOTPDeviceMixin
tokens
class ThrottlingTestCase(TOTPDeviceMixin, authentik.stages.authenticator.tests.ThrottlingTestMixin, django.test.testcases.TestCase):
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.