-
-
Save VertigoRay/6a81325921e828dec7c754f0ddf0aa4c to your computer and use it in GitHub Desktop.
C:\Temp>env\Scripts\python.exe manage.py shell | |
Python 3.5.1 (v3.5.1:37a07cee5969, Dec 6 2015, 01:54:25) [MSC v.1900 64 bit (AMD64)] on win32 | |
Type "help", "copyright", "credits" or "license" for more information. | |
(InteractiveConsole) | |
>>> from myapp.settings import EMAIL_KEY_EXPIRY_TIME | |
>>> from user.helpers import signing_dumps_w_entropy, signing_loads_w_entropy, get_uri | |
>>> import urllib.parse | |
>>> | |
>>> email = '[email protected]' | |
>>> key = signing_dumps_w_entropy(email) | |
>>> key | |
'IlZlcnRpZ29SYXlAZXhhbXBsZS5jb218YTIzd0lKekk4clhlVkVuSlhOdEt4N1kyYyI:1b2RGO:QuCLsDJvmTznZv-4IRrOKms6KAw' | |
>>> | |
>>> url = 'http://example.com/user/validate_email/{0}'.format( | |
... urllib.parse.quote(key), | |
... ) | |
>>> url | |
'http://example.com/user/validate_email/IlZlcnRpZ29SYXlAZXhhbXBsZS5jb218YTIzd0lKekk4clhlVkVuSlhOdEt4N1kyYyI%3A1b2RGO%3AQuCLsDJvmTznZv-4IRrOKms6KAw' | |
>>> | |
>>> # That's the url that we'll send via email | |
>>> # When a user clicks the link, `urls.py` will send the key portion as `key`; such as: | |
>>> # url(r'^validate_email/(?P<key>[a-zA-Z0-9-_=:]+)$', validate_email, name='validate_email'), | |
>>> | |
>>> key | |
'IlZlcnRpZ29SYXlAZXhhbXBsZS5jb218YTIzd0lKekk4clhlVkVuSlhOdEt4N1kyYyI:1b2RGO:QuCLsDJvmTznZv-4IRrOKms6KAw' | |
>>> signing_loads_w_entropy(key, max_age=EMAIL_KEY_EXPIRY_TIME) | |
'[email protected]' | |
>>> | |
>>> # At this point, we can lookup the account with this e-mail address | |
>>> # and mark it as validated in the database. | |
>>> | |
>>> # A couple more things ... | |
>>> # Let's break the key | |
>>> # (pretend word wrap wrapped the last character and a user messed up the copy & paste): | |
>>> email = signing_loads_w_entropy(key[:-1], max_age=EMAIL_KEY_EXPIRY_TIME) | |
django.core.signing.BadSignature: Signature "QuCLsDJvmTznZv-4IRrOKms6KA" does not match | |
>>> | |
>>> # Let's use an expired key | |
>>> from datetime import timedelta | |
>>> signing_loads_w_entropy(key, max_age=timedelta(seconds=1)) | |
django.core.signing.SignatureExpired: Signature age 412.71953558921814 > 1.0 seconds |
from django.core import signing | |
from string import ascii_lowercase, ascii_uppercase, digits | |
import random | |
def signing_dumps_w_entropy(string): | |
""" | |
This function just adds some entropy to the django.core.signing.dumps function. | |
""" | |
nonce = ''.join(random.SystemRandom().choice(ascii_lowercase + ascii_uppercase + digits) for _ in range(25)) | |
string_w_entropy = '|'.join((string, nonce)) | |
return signing.dumps(string_w_entropy, compress=True) | |
def signing_loads_w_entropy(string, max_age=None): | |
""" | |
This function just removes entropy to the django.core.signing.dumps function. | |
""" | |
return signing.loads(string, max_age=max_age).rsplit('|', 1)[0] | |
def get_uri(request, force_secure=False): | |
""" | |
Get the current URI; ie: http://localhost | |
""" | |
if force_secure or request.is_secure(): | |
return 'https://%s' % request.get_host() | |
else: | |
return 'http://%s' % request.get_host() |
# myapp/settings.py | |
# Removed everything except what's needed for this demo. | |
from datetime import timedelta | |
EMAIL_KEY_EXPIRY_TIME = timedelta(days=2) |
I like the idea of adding some random data, so that no pattern, not even the time based one, shows up. Some thoughts:
-
You aren't building a full uri, just the scheme/hosts. I would handle the https stuff differently: have nginx redirect all requests to https, and then just use build_absolute_uri
-
"one|two|three|four".rsplit("|", 1)[0] = "one|two|three"
for a simpler. Your decoding function could be:return signing.loads(value, max_age=max_age).rsplit("|", 1)[0]
-
One interesting thing (since you seem very worried about this) would be to stick the user's IP in there and check. Of course, issues with proxies/etc abound.
In general though it looks fine. I do feel you are a little over worried about this, considering how unlikely it is to be broken. One other thing you can do is encode a json string, in which case you are guaranteed that if you can encode it you can decode it. It gets large though, but you could encode other information in it.
Decided to add back a couple of helper functions so that the key sent to the user had some entropy. This will help to prevent anyone from eventually decoding my SECRET_KEY; since the result isn't just their email address..