I wanted to put this out here and see if there were any security vulnerabilities that I'm not seeing. Also, if I can improve performance without sacrificing security, that's always a plus. Thanks in advance!
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? Write handlers so this data doesn't show up the user profile? I wanted to use a disposable key
that didn't have to be stored in a database. This is my solution; which is also available on gist ...
# user/helpers.py
from django.core.signing import BadSignature, Signer
import base64
import json
import time
def email_validation_encode(payload):
"""
I expect the payload to be something like:
{
'email': '[email protected]',
'expires': (a few days from now),
}
However, I don't care enough to validate
"""
payload_json = json.dumps(payload)
payload_b64 = base64.b64encode(payload_json.encode('utf-8'))
signer = Signer()
return signer.sign(payload_b64.decode('utf-8'))
def email_validation_decode(string):
"""
Expects the result from the `email_validation_encode` function.
If the string looks valid: returns the original payload
If anything is changed/invalid: returns False
"""
signer = Signer()
try:
string_b64 = signer.unsign(string)
except BadSignature:
return False
json_decoded = base64.b64decode(string_b64).decode('utf-8')
return json.loads(json_decoded)
def create_email_validation_key(email):
"""
Makes an object:
{
'email': '[email protected]',
'expires': (two or three days from now),
}
returns the key to put in the link to send to email
"""
return email_validation_encode({
'email': email,
'expires': (time.time() + 60*60*24*2),
})
def decode_email_validation_key(key):
"""
This takes the key (from link sent to email), sends it to be decoded.
Then, it checks the expiration.
If the key is valid and hasn't expired: returns just the email address
If anything is changed/invalid: returns False
"""
decoded = email_validation_decode(key)
if not decoded or time.time() > decoded['expires']:
return False
else:
return decoded['email']
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()
Implementation
Implementation would obviously be within my user/views.py
file, but you'll get the idea from this excerpt, I think:
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 user.helpers import create_email_validation_key, decode_email_validation_key, get_uri
>>> import urllib.parse
>>>
>>> key = create_email_validation_key('[email protected]')
>>> key
'eyJlbWFpbCI6ICJWZXJ0aWdvUmF5QGV4YW1wbGUuY29tIiwgImV4cGlyZXMiOiAxNDYzNDU2MzgwLjA4NDAyNjN9:xzklS0YL3NT-GR33Nilo'
>>>
>>> url = 'http://example.com/user/register/{0}'.format(
... urllib.parse.quote(key),
... )
>>> url
'http://example.com/user/register/eyJlbWFpbCI6ICJWZXJ0aWdvUmF5QGV4YW1wbGUuY29tIiwgImV4cGlyZXMiOiAxNDYzNDU2MzgwLjA4NDAyNjN9%3AxzklS0YL3NT-GR33Nilo'
>>>
>>> # 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-_=]+[:][a-zA-Z0-9-_=]+)$', validate_email, name='validate_email'),
>>>
>>> key
'eyJlbWFpbCI6ICJWZXJ0aWdvUmF5QGV4YW1wbGUuY29tIiwgImV4cGlyZXMiOiAxNDYzNDU2MzgwLjA4NDAyNjN9:xzklS0YL3NT-GR33Nilo'
>>> email = decode_email_validation_key(key)
>>> email
'[email protected]'
>>>
>>> # At this point, we can lookup the account with this e-mail address
>>> # and mark it as validated in the database.
>>>
>>> # One more thing, let's break the key:
>>> email = decode_email_validation_key(key[1:])
>>> email
False
Note: I would actually set the url
as follows, but I wanted to just focus on the functions related to this demo. I'm only showing you this additional bit to demo the usage of the get_uri()
function; in case you can give me a better way to do this:
url = '{0}/{1}/{2}'.format(
get_uri(request),
reverse('register'),
urllib.parse.quote(key),
)
Update: changed from pickle to json (https://youtu.be/7KnfGDajDQw). I tend to prefer json, but I didn't want to convert datetime.datetime.now()
to something json would parse. A good friend told me to use time.time()
, and I felt like a n00b. I'm enjoying the shorter keys as well.