1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
# vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models, IntegrityError
from django.utils.timezone import now
from .crypto import idcipher
from datetime import timedelta
# based on https://gist.github.com/treyhunner/735861
class EncryptedPKModelManager(models.Manager):
def get(self, *args, **kwargs):
eid = kwargs.pop('encrypted_id', None)
if eid is not None:
kwargs['id'] = idcipher.decrypt(eid)
return super(EncryptedPKModelManager, self).get(*args, **kwargs)
class EncryptedPKModel(models.Model):
"""
A model with built-in identifier encryption (for secure tokens).
"""
objects = EncryptedPKModelManager()
@property
def encrypted_id(self):
"""
The object identifier encrypted using IDCipher, as a hex-string.
"""
if self.id is None:
return None
return idcipher.encrypt(self.id)
class Meta:
abstract = True
class RevokedToken(models.Model):
"""
A model that guarantees atomic token revocation.
We can use a single table for various kinds of tokens as long
as they don't interfere (e.g. are of different length).
"""
user = models.ForeignKey(User, db_index=False, null=True)
token = models.CharField(max_length=64)
ts = models.DateTimeField(auto_now_add=True)
@classmethod
def cleanup(cls):
"""
Remove tokens old enough to be no longer valid.
"""
# we use this just to enforce atomicity and prevent replay
# for SOTP, we can clean up old tokens quite fast
# (as soon as .delete() is effective)
# for TOTP, we should wait till the token drifts away
old = now() - timedelta(minutes=3)
cls.objects.filter(ts__lt=old).delete()
@classmethod
def add(cls, token, user=None):
"""
Use and revoke the given token, for the given user. User
can be None if irrelevant.
Returns True if the token is fine, False if it was used
already.
"""
cls.cleanup()
t = cls(user=user, token=token)
try:
t.save()
except IntegrityError:
return False
return True
class Meta:
unique_together = ('user', 'token')
|