0
# Device Models and Management
1
2
Abstract base models and mixins that provide the foundation for all OTP devices, including security features like throttling, cooldowns, and timestamp tracking.
3
4
## Capabilities
5
6
### Abstract Base Models
7
8
#### Device
9
10
The abstract base model for all OTP devices, providing core functionality for device management and token verification.
11
12
```python { .api }
13
class Device(models.Model):
14
"""
15
Abstract base model for OTP devices.
16
17
Fields:
18
- user: ForeignKey to User model - The user this device belongs to
19
- name: CharField(max_length=64) - Human-readable device name
20
- confirmed: BooleanField(default=True) - Whether device is confirmed/active
21
"""
22
23
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
24
name = models.CharField(max_length=64)
25
confirmed = models.BooleanField(default=True)
26
27
# Properties
28
@property
29
def persistent_id(self) -> str:
30
"""Stable device identifier for session storage."""
31
32
# Class methods
33
@classmethod
34
def model_label(cls) -> str:
35
"""Returns the model label string."""
36
37
@classmethod
38
def from_persistent_id(cls, persistent_id, for_verify=False):
39
"""Load device from persistent ID."""
40
41
# Instance methods
42
def is_interactive(self) -> bool:
43
"""Returns True if device supports challenges."""
44
45
def generate_is_allowed(self):
46
"""Check if challenge generation is allowed."""
47
48
def generate_challenge(self):
49
"""Generate a challenge for the user."""
50
51
def verify_is_allowed(self):
52
"""Check if token verification is allowed."""
53
54
def verify_token(self, token) -> bool:
55
"""Verify an OTP token."""
56
```
57
58
#### SideChannelDevice
59
60
Abstract base model for devices that send tokens through side channels (email, SMS, etc.).
61
62
```python { .api }
63
class SideChannelDevice(Device):
64
"""
65
Abstract base model for side-channel devices.
66
67
Fields:
68
- token: CharField(max_length=16) - Current token value
69
- valid_until: DateTimeField - Token expiration timestamp
70
"""
71
72
token = models.CharField(max_length=16)
73
valid_until = models.DateTimeField(default=timezone.now)
74
75
def generate_token(self, length=6, valid_secs=300, commit=True):
76
"""
77
Generate a new token for this device.
78
79
Parameters:
80
- length: int - Token length in characters
81
- valid_secs: int - Token validity in seconds
82
- commit: bool - Whether to save the device immediately
83
84
Returns:
85
str - The generated token
86
"""
87
88
def verify_token(self, token) -> bool:
89
"""
90
Verify token by content and expiry.
91
92
Parameters:
93
- token: str - Token to verify
94
95
Returns:
96
bool - True if token is valid and not expired
97
"""
98
```
99
100
### Mixins
101
102
#### CooldownMixin
103
104
Adds cooldown functionality to prevent rapid challenge generation.
105
106
```python { .api }
107
class CooldownMixin(models.Model):
108
"""
109
Mixin that adds cooldown functionality between challenge generations.
110
111
Fields:
112
- last_generated_timestamp: DateTimeField - Last generation time
113
"""
114
115
last_generated_timestamp = models.DateTimeField(null=True, default=None)
116
117
@property
118
def cooldown_enabled(self) -> bool:
119
"""Returns True if cooldown duration > 0."""
120
121
def generate_is_allowed(self):
122
"""Check if generation is allowed based on cooldown status."""
123
124
def cooldown_reset(self, commit=True):
125
"""Reset cooldown by clearing the timestamp."""
126
127
def cooldown_set(self, commit=True):
128
"""Set cooldown timestamp to current time."""
129
130
def get_cooldown_duration(self) -> int:
131
"""Abstract method to return cooldown duration in seconds."""
132
```
133
134
#### ThrottlingMixin
135
136
Adds exponential back-off for failed verification attempts.
137
138
```python { .api }
139
class ThrottlingMixin(models.Model):
140
"""
141
Mixin that adds exponential back-off for failed verifications.
142
143
Fields:
144
- throttling_failure_timestamp: DateTimeField - Last failure time
145
- throttling_failure_count: PositiveIntegerField - Consecutive failures
146
"""
147
148
throttling_failure_timestamp = models.DateTimeField(null=True, default=None)
149
throttling_failure_count = models.PositiveIntegerField(default=0)
150
151
@property
152
def throttling_enabled(self) -> bool:
153
"""Returns True if throttle factor > 0."""
154
155
def verify_is_allowed(self):
156
"""Check if verification is allowed based on throttling status."""
157
158
def throttle_reset(self, commit=True):
159
"""Reset throttling by clearing failure count and timestamp."""
160
161
def throttle_increment(self, commit=True):
162
"""Increment failure count and update timestamp."""
163
164
def get_throttle_factor(self) -> int:
165
"""Abstract method to return throttle factor."""
166
```
167
168
#### TimestampMixin
169
170
Adds creation and last-used timestamps to devices.
171
172
```python { .api }
173
class TimestampMixin(models.Model):
174
"""
175
Mixin that adds creation and usage timestamps.
176
177
Fields:
178
- created_at: DateTimeField(auto_now_add=True) - Creation time
179
- last_used_at: DateTimeField(null=True) - Last usage time
180
"""
181
182
created_at = models.DateTimeField(auto_now_add=True)
183
last_used_at = models.DateTimeField(null=True, default=None)
184
185
def set_last_used_timestamp(self, commit=True):
186
"""Update last used timestamp to current time."""
187
```
188
189
### Managers
190
191
#### DeviceManager
192
193
Manager class for Device models with custom query methods.
194
195
```python { .api }
196
class DeviceManager(models.Manager):
197
"""Manager for Device models."""
198
199
def devices_for_user(self, user, confirmed=None):
200
"""
201
Returns queryset for user's devices.
202
203
Parameters:
204
- user: User - The user whose devices to retrieve
205
- confirmed: bool or None - Filter by confirmation status
206
207
Returns:
208
QuerySet - Filtered device queryset
209
"""
210
```
211
212
### Enums
213
214
#### GenerateNotAllowed
215
216
Constants for challenge generation restriction reasons.
217
218
```python { .api }
219
class GenerateNotAllowed(Enum):
220
"""Enum for generation restriction reasons."""
221
COOLDOWN_DURATION_PENDING = 'COOLDOWN_DURATION_PENDING'
222
```
223
224
#### VerifyNotAllowed
225
226
Constants for token verification restriction reasons.
227
228
```python { .api }
229
class VerifyNotAllowed(Enum):
230
"""Enum for verification restriction reasons."""
231
N_FAILED_ATTEMPTS = 'N_FAILED_ATTEMPTS'
232
```
233
234
## Usage Examples
235
236
### Creating a Custom Device Type
237
238
```python
239
from django_otp.models import Device, ThrottlingMixin, TimestampMixin
240
from django.db import models
241
242
class CustomDevice(TimestampMixin, ThrottlingMixin, Device):
243
"""Custom OTP device implementation."""
244
245
secret_key = models.CharField(max_length=32)
246
247
class Meta:
248
verbose_name = "Custom OTP Device"
249
250
def verify_token(self, token):
251
# Custom verification logic
252
if self.verify_is_allowed() != True:
253
return False
254
255
# Your token verification logic here
256
is_valid = self._verify_custom_token(token)
257
258
if is_valid:
259
self.throttle_reset()
260
self.set_last_used_timestamp()
261
return True
262
else:
263
self.throttle_increment()
264
return False
265
266
def get_throttle_factor(self):
267
return 1 # 1 second base throttle
268
269
def _verify_custom_token(self, token):
270
# Implement your verification logic
271
pass
272
```
273
274
### Using Device Mixins
275
276
```python
277
from django_otp.models import SideChannelDevice, CooldownMixin, ThrottlingMixin
278
279
class EmailOTPDevice(CooldownMixin, ThrottlingMixin, SideChannelDevice):
280
"""Email-based OTP device with cooldown and throttling."""
281
282
email = models.EmailField()
283
284
def generate_challenge(self):
285
if self.generate_is_allowed() != True:
286
return False
287
288
# Generate and send token
289
token = self.generate_token()
290
self.send_email(token)
291
self.cooldown_set()
292
return True
293
294
def get_cooldown_duration(self):
295
return 60 # 60 second cooldown between emails
296
297
def get_throttle_factor(self):
298
return 2 # 2 second base throttle, exponential backoff
299
```
300
301
### Device Discovery and Management
302
303
```python
304
from django_otp.models import Device
305
306
def get_device_info(device):
307
"""Get comprehensive device information."""
308
info = {
309
'id': device.persistent_id,
310
'name': device.name,
311
'type': device.__class__.__name__,
312
'confirmed': device.confirmed,
313
'model_label': device.model_label(),
314
'is_interactive': device.is_interactive(),
315
}
316
317
# Add timestamp info if available
318
if hasattr(device, 'created_at'):
319
info['created_at'] = device.created_at
320
if hasattr(device, 'last_used_at'):
321
info['last_used_at'] = device.last_used_at
322
323
# Add security status
324
if hasattr(device, 'throttling_enabled'):
325
info['throttling_enabled'] = device.throttling_enabled
326
if hasattr(device, 'cooldown_enabled'):
327
info['cooldown_enabled'] = device.cooldown_enabled
328
329
return info
330
```