Skip to content

Commit 0a18fb3

Browse files
Improve single use with settings and more docs
1 parent 3669ffa commit 0a18fb3

File tree

4 files changed

+27
-16
lines changed

4 files changed

+27
-16
lines changed

docs/settings.rst

+8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ Mail Auth settings
1717
Defines the URL the user will be redirected to, after requesting an
1818
authentication message.
1919

20+
.. attribute:: LOGIN_TOKEN_SINGLE_USE
21+
22+
Default: ``True``
23+
24+
Defines if a token can be used more than once.
25+
If ``True``, the same token can only be used once and will be invalid the next try.
26+
If ``False``, the same token can be used multiple times and remains valid until expired.
27+
2028
Django related settings
2129
-----------------------
2230

mailauth/backends.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ class MailAuthBackend(ModelBackend):
1212

1313
def authenticate(self, request, token=None):
1414
max_age = getattr(settings, 'LOGIN_URL_TIMEOUT', 60 * 15)
15+
single_use = getattr(settings, 'LOGIN_TOKEN_SINGLE_USE', True)
1516

1617
try:
17-
user = self.signer.unsign(token, max_age=max_age)
18+
user = self.signer.unsign(token, max_age=max_age, single_use=single_use)
1819
except (get_user_model().DoesNotExist, BadSignature):
1920
return
2021
else:

mailauth/signing.py

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from django.contrib.auth import get_user_model
22
from django.core import signing
3-
from django.core.signing import SignatureExpired
43
from django.utils import baseconv
54

65
__all__ = (
@@ -53,15 +52,19 @@ def _make_hash_value(self, user):
5352
user_pk = baseconv.base62.encode(user.pk)
5453
return self.sep.join((user_pk, last_login))
5554

56-
def unsign(self, value, max_age=None, allow_multi_use=False):
55+
def unsign(self, value, max_age=None, single_use=True):
5756
"""
5857
Verify access token and return user, if the token is valid.
5958
6059
Args:
6160
value (str): URL safe base64 encoded access token.
6261
max_age (datetime.timedelta): Maximum age an access token to be valid.
63-
allow_multi_use: If True allows the token to be used more than once
64-
62+
single_use (bool):
63+
If ``True``, the same token can only be used once and will be invalid
64+
the next try.
65+
If ``False``, the same token can be used multiple times and remains
66+
valid until expired.
67+
Default: ``True``
6568
Returns:
6669
django.contrib.user.models.BaseUser: Return user object for given
6770
access token.
@@ -86,11 +89,9 @@ def unsign(self, value, max_age=None, allow_multi_use=False):
8689
except get_user_model().DoesNotExist as e:
8790
raise UserDoesNotExist("User with pk=%s does not exist" % user_pk) from e
8891
else:
89-
if last_login != '' and self.to_timestamp(user.last_login) != last_login:
90-
if allow_multi_use:
91-
return user
92-
else:
93-
raise SignatureExpired(
94-
"The access token for %r seems used" % user
95-
)
92+
if (single_use and last_login != '' and
93+
self.to_timestamp(user.last_login) != last_login):
94+
raise signing.SignatureExpired(
95+
"The access token for %r seems used" % user
96+
)
9697
return user

tests/test_signing.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,21 @@ def test_unsign__last_login(self, db, signer, signature):
3939
):
4040
signer.unsign(signature)
4141

42-
def test_unsing__allow_multiple_use(self, db, signer, signature):
42+
def test_unsing__single_use(self, db, signer, signature):
4343
user = get_user_model().objects.create_user(
4444
pk=1337,
4545
email='spiderman@avengers.com',
4646
# later date, that does not match the signature (token was used)
4747
last_login=timezone.datetime(2012, 7, 3, tzinfo=timezone.utc),
4848
)
49-
assert user == signer.unsign(signature, allow_multi_use=True)
50-
assert user == signer.unsign(signature, allow_multi_use=True)
49+
assert user == signer.unsign(signature, single_use=False)
50+
# test a second time to make sure token can be used more than one time
51+
assert user == signer.unsign(signature, single_use=False)
5152
with pytest.raises(
5253
SignatureExpired,
5354
match="The access token for <EmailUser: spiderman@avengers.com> seems used"
5455
):
55-
signer.unsign(signature, allow_multi_use=False)
56+
signer.unsign(signature, single_use=True)
5657

5758
def test_to_timestamp(self):
5859
value = timezone.datetime(2002, 5, 3, tzinfo=timezone.utc)

0 commit comments

Comments
 (0)