Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save VertigoRay/6a81325921e828dec7c754f0ddf0aa4c to your computer and use it in GitHub Desktop.
Save VertigoRay/6a81325921e828dec7c754f0ddf0aa4c to your computer and use it in GitHub Desktop.
Typically, when you want to validate an e-mail address, you store an activation key and expiration date in the user profile table. So you can validate it one time. Then what? Just keep storing it forever? Clear it out and have the empty columns? I wanted a disposable key that didn't have to be stored in a database. This is my solution ... http:/…
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 = 'VertigoRay@example.com'
>>> 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)
'VertigoRay@example.com'
>>>
>>> # 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)
@awbacker
Copy link

awbacker commented May 17, 2016

I like the idea of adding some random data, so that no pattern, not even the time based one, shows up. Some thoughts:

  1. 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

  2. "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]
  3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment