Who needs a database?

Posted on 20 March 2014 by Joseph

Isn't it funny how all of the boring, extraneous parts of a project often add up to take as much time as the central parts? Like, not only do you have to solve your problem, but you also have to handle accounts, and send emails, and write a privacy statement, and a million other things like that. It's pretty rare that you get to have fun doing these less exciting parts, so when I got the chance to, I jumped.

Last week I read this article, and I thought that the basic idea was great: eliminate a lot of complexity, boilerplate code, and repetition by being a little more clever. Specifically, the article outlined a method for handling events like password resets, in which you typically send a URL containing a hash to the client, that didn't require the use of a database. Intriguing? Let me summarize.

Normally, you'd generate a random hash, throw it into the database along with some metadata like when it was generated, then tack that onto the URL you sent the user. This obviously requires a database table and multiple database queries, along with all the code required to manage those things. Yuck.

Instead, the author advocated digitally authenticating the payload of the message with a secret key and including the payload and key in the URL itself. When the user visits the URL, your app regenerates the key given the payload and checks for validity. If the keys match, you have a valid request. He even included a way to handle expiring the authenticated messages and making the URLs one-use.

OK, now do it with Python

Like I said earlier, I jumped at the opportunity. Let's build this system in Python using Flask. Flask is a particularly appropriate choice, since the author of the article above, Armin Ronacher founded the Flask project.

First, let's start with a minimal Flask boilerplate. (Feeling impatient? You can get all the code from this article here)

 1 from flask import Flask, request
 2 app = Flask(__name__)
 3 
 4 
 5 @app.route("/", methods=['GET', 'POST'])
 6 def passreset():
 7     if request.method == 'GET':
 8         return """
 9             <p>
10                 Forget your password?
11                 Enter your email below to reset it.
12             </p>
13             <form method='POST'>
14                 <input type='text' name='reset_email' />
15                 <button type='submit'>Reset your password</button>
16             </form>"""
17     else:
18         # Here is where you'd normally do all the nasty databse stuffs
19         return request.form.get('reset_email')
20 
21 
22 if __name__ == "__main__":
23     app.run(debug=True, host='0.0.0.0')

As it suggests, when the user submits the form, you'd normally generate the hash, collect the metadata, put all that in a database row, generate the URL and send the email. Not us though! Instead, what we'll do is create a payload, create an authentication code, and build the URL from that.

Build the payload

1 def create_payload(email):
2     payload = { 
3         "email": email
4     } 
5     return payload

Nothing fancy yet, we just want to know who we're resetting when the link is visited. Remember, we have no information about this request other than the payload, so include any information you will need!

Create the authentication code

1 import hmac
2 import json
3 
4 def get_auth_code(payload):
5     return hmac.new(app.config['SECRET_KEY'], json.dumps(payload)).hexdigest()

We're using a hash-based message authentication code (HMAC) as the authentication code. A HMAC is a strong way to combine a secret key (in this case, the Flask application secret key, though it doesn't need to be) with a payload. If you'd like to know more, you can read the Design Principles section of the Wikipedia article for more information on why it's strong and why you'd want that. Since the HMAC only hashes strings, we are dumping to a JSON string prior to computing the HMAC.

Form the URL

Now lets put the two functions together to create our URL. This function will return the GET string we need to include in our custom URL.

1 import urllib
2 
3 def get_url_keys(payload):
4     url_keys = { 
5         'auth_code': get_auth_code(payload)
6     } 
7     url_keys.update(payload)
8     return urllib.urlencode(url_keys)

With these functions in hand, let's rebuild the else clause of our route from the boilerplate earlier.

 1     else:  # From line number 17 in the boilerplate
 2         payload = create_payload(request.form['reset_email'])
 3         url = "/passreset?%s" % get_url_keys(payload)
 4         return """
 5             <p>
 6                 This should really be in an email!
 7                 Use the link below to reset your password.
 8             </p>
 9             <a href="%(url)s">
10                 %(url)s
11             </a>""" % {"url": url}

Obviously, as suggested by the page itself, don't do this in real life. Instead send the URL (complete with your domain! I have been burned by this in real emails) to your user via email.

Where do you wind up?

All that's left is to build a route for the reset URLs and parse them. Here's a function for checking the authenticity of the message.

1 class PassResetError(Exception):
2     pass
3 
4 
5 def check_passreset(payload, auth_code):
6     new_code = get_auth_code(payload)
7     if new_code != auth_code:
8         raise PassResetError("Invalid password reset request")

All it does is recompute the authentication code and compare it to the one it is given.

Here's the view for the new route.

 1 @app.route("/passreset")
 2 def newpass():
 3     # Need to check for both methods
 4     try:
 5         payload = {
 6             "email": str(request.args['email']),
 7         }
 8         auth_code = str(request.args['auth_code'])
 9         check_passreset(payload, auth_code)
10     except KeyError:
11         # Check for missing form elements
12         return "Invalid password reset request (form elements)"
13     except PassResetError, e:
14         return str(e)
15     # We're in the clear, give them the password reset form
16     return "Good to go! Here's where I'd render a new password form."

The view extracts the keys from the GET string in the url (request.args) and handles missing values. It then calls the verification function above and either renders a password reset form (ahem) or returns an error. You can try it yourself by changing a character or two of the auth_code or email in the URL in your location bar.

From here you should be able to integrate this into your own models and endpoints. Like I mentioned above, you can find all the code from this article here. Though we built this in the context of a password reset form, this same concept extends to any hashed URL operation, like email verifications.

Next time we'll take a look at two extensions: expiring requests and one-use requests.

comments powered by Disqus