or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

admin-interface.mdcore-authentication.mddevice-models.mddjango-integration.mdemail-devices.mdhotp-devices.mdindex.mdoath-algorithms.mdstatic-tokens.mdtotp-devices.md

device-models.mddocs/

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

```