# Validating Email Addresses without DB

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

import base64
import json
import time

"""
I expect the payload to be something like:
{
'email': 'me@example.com',
'expires': (a few days from now),
}
However, I don't care enough to validate
"""

signer = Signer()

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)
return False

json_decoded = base64.b64decode(string_b64).decode('utf-8')

def create_email_validation_key(email):
"""
Makes an object:
{
'email': 'me@example.com',
'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
(InteractiveConsole)
>>> from user.helpers import create_email_validation_key, decode_email_validation_key, get_uri
>>> import urllib.parse
>>>
>>> key = create_email_validation_key('VertigoRay@example.com')
>>> 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
'VertigoRay@example.com'
>>>
>>> # 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.

• So, are you looking for the code to be reviewed? What do you want the review to focus on? (Security/performance/etc.)? – Kevin Brown May 15 '16 at 3:06
• Security is paramount here, but I definitely would like to keep performance in mind. – VertigoRay May 15 '16 at 3:08
• Why not use a timestamped signer? Seems like that's what you are emulating: docs.djangoproject.com/en/1.9/topics/signing/… – Sjoerd Job Postmus May 15 '16 at 6:34

1. As Sjoerd points out in comments, the timestamp generation and checking logic in this code is reimplementing Django's built-in TimestampSigner. Standard implementations are more likely to be correct (they've had more review and testing) and they are more likely to get fixed quickly if problems are discovered.

2. Similarly, the serialization and deserialization logic in this code is reimplementing Django's built-in signing.dumps and signing.loads.

3. decode_email_validation_key returns a string (the e-mail address) if verification is successful, or False if it fails. This is risky: it would be easy to forget to check the result. It would be better to raise an exception in case of failure. Then if you forget to check the exception, it doesn't open a security hole.

4. The expiry time (here, 60*60*24*2) should have a name, and it should be defined in the application settings file, since you'll probably want to use it in the construction of the e-mail text to tell the user when the link expires.

Revised code (settings file):

from datetime import timedelta

EMAIL_KEY_EXPIRY_TIME = timedelta(days=2)


Revised code (view file):

from myapp.settings import EMAIL_KEY_EXPIRY_TIME
from django.core import signing

create_email_validation_key = signing.dumps

def decode_email_validation_key(key):