danbirken.com

home

Simple Automatic Login From Emails Using A Key/Value Store

07 Jan 2015

When I click a link on my mobile email client and get a login prompt, there is 0% chance I am continuing to use your website.

So I thought instead of continuing to complain about how password prompts are annoying, I'd make a technical post about a simple approach to add automatic login to links in emails your website sends.

By the end of this post we will have a fully functional python application that sends emails, and then when you click on them it logs you into the website. This app will be simple, scalable, secure-ish and will fail gracefully. We'll grade the application on these assurances at the bottom of this post.

General Overview

When my app sends an email, I will append a special code to end of every link. For example, if my email was linking to http://www.danbirken.com/, my app will change that link to http://www.danbirken.com/?email_login_code=abcdefg.

When somebody visits my website, my app will check to see if the URL has the email_login_code query parameter set. If so, it will check this code, and if valid it will automatically log that person in.

Step 1: Creating the login code

To create the code, we just need to generate a random series of random email and URL friendly characters. For maximum compatibility, I'll choose only the characters a-z and I'll do a string of 20 of them. That will be hard to brute-force.

def generate_random_string(length=20, chars=None):
    if chars is None:
        chars = 'abcdefghijklmnopqrstuvwxyz'

    return ''.join(random.choice(chars) for _ in range(length))

[reference]

Step 2: Adding it to outgoing emails

For my example app, I'm generating my emails with jinja2 templates. So I'm going to make a jinja2 filter that will automatically add in the login code to links I include in emails. A link with auto login code attached will look like this:

<a href="{{ '/'|full_url|login_code }}">Login Link</a>

[reference]

There are two filters here, full_url and login_code. The full_url filter adds in the domain to the link (because unlike relative links within a website, the email client needs to know the domain). The login_code filter looks for the email_login_code parameter in the template context, and if defined it will add it as a query parameter to the link.

So in combination these filters will turn a link from / to

http://www.danbirken.com/?email_login_code=abcdefg

[reference]

Step 3: Storing the login code

So we generated the code and added it to the outgoing email, but how do we verify it? Well, this is where a key-value store comes in. Before we send the email, we save the code into a key-value store along with information needed to log them in (ie, their email address or user id).

class EmailCodeAuth():
    def __init__(self, kv_store):
        self.kv_store = kv_store

    def generate_code(self, email, expire_seconds=86400):
        code = self.generate_random_string()
        self.kv_store.set(code, email, time=expire_seconds)
        return code

    def validate_code(self, code):
        return self.kv_store.get(code)

[reference]

Step 4: Validating the login code

Then when a request comes in to your website, pull out the code and verify it to see if it is valid:

email = None
if 'auto_login_code' in request.args:
    email = self.email_auth.validate_code(
        request.args['auto_login_code']
    )

[reference]

If so, log them in. If not, do nothing.

Grading this approach

In the introduction I said this app would be simple, scalable, secure-ish and will fail gracefully.

Simple

This is hard to grade in the absolute sense, but this entire app (including web server and email sending code) is under 200 lines of code. That seems pretty simple to me.

Scalable

The magic of this approach is we are just piggybacking off of the technology in the various key/value stores out there. Assuming each of these key/value pairs can be stored in 64 bytes (which is very conservative), then we can store well over 10 million of these pairs per 1 GB of memory.

There are so many good options for a key/value store you really can't go wrong, but I'll single out memcached for absolute simplicity. But for your particular needs you might want to use redis or Amazon DynamoDB or postgres or another one depending on your website's architecture.

Regardless of which key/value store you use, a single key lookup (per page load) or key store (per email sent) should be one of the cheapest aspects of completing that action.

Fail gracefully

If our key-value store breaks for some reason, no big deal. It certainly would be nice if our auto-login feature were working, but due to that fact that we are just appending the code as part of the query string, the user will still get sent to the right place regardless of whether the auto-login is working or not.

Secure-ish

There are multiple aspects of the security that we'll look at. As a preface, if your application handles sensitive information then auto-login via email probably isn't for you. But I think for most sites the value of being really convenient for your users is more valuable than being incredibly secure (example from my previous post: okcupid).

Expiring of codes

It is a good idea to expire the codes at some point for a couple of reasons. From a security perspective, we want to be sure that somebody who is logging in has had recent access to the email account. From a computing resources perspective, we don't want to waste a bunch of space storing codes from a year ago.

Luckily, most of our key/value stores do this for us automatically. Just look up the interface for setting keys and you will likely find a field for setting the expiration date. Something like 2-7 days is probably a good initial value.

Brute forcing of codes

20 random characters from a-z is a lot of possible codes (exercise left to the reader). Brute forcing isn't going to work. If paranoid, use a wider variety of characters or make it the code longer.

But email isn't secure

If you have a forgot password section of your website that allows a user to login directly from a received email, then this really isn't any more insecure than that.

Additionally, a reasonable extension of this simple approach would be a multi-tiered login level, in which somebody who logged in via email doesn't have access to certain sensitive account settings (changing email, changing password, etc) until they enter their password.

Source Code

The full source code is available at danbirken/email-auto-login and it should be fairly easy to run locally (requires python3, werkzeug and jinja2).

This is not designed to be code that can be used as a library for your application, it's just a very simple proof of concept of the approach.