lukeplant.me.uk

A simple password-less, email-only login system

Posted in: Django, Python, Web development

This post is about a simple password-less login system I created for one web site which can be useful in some use cases. I’ll describe the basic process, the rationale, and the advantages and disadvantages of the system. Then I’ll outline some implementation considerations, and link to my source code which implements it.

Outline

The authentication system is simply this:

To log in, a user enters their email address. The web site sends them an email containing a unique link which will directly log them in to the site. There is no option to use a password. If they have used the site in the past with the same email address, upon logging in they will be using the same account as before, otherwise a new account will be created. Every time the user wants to log in, they must go through the same process (so usually you will make the login session last a significant period of time). For this reason the method is particularly suited to sites where people do not log in very often.

This is not a new idea, although I don’t think I was conscious of other implementations when I created mine. In this post I’m presenting my rationale for it, listing some advantages that I haven’t seen elsewhere, and other implementation pointers.

Rationale

  1. Many systems really need a working email address, because you need to be able to contact users. In this case you have to do some kind of email verification step at some point anyway (some systems do it at the beginning of the process, others try to fit it in somewhere else and nag users until they have done it). If you fail to have email verification, then people can easily get locked out of the site because password reset usually relies on sending an email, and you don’t have contact details when you need them.

    With this system, email verification and login are combined.

  2. In terms of security from the user’s point of view, no-one can hack their account by guessing their password, because they don’t have a password. They can hack it only by gaining access to their email, but given the password reset mechanism most sites have, this is no different to normal. We’ve simply eliminated one source of getting hacked.

  3. For the site implementation, not having a password to store is even better — there is no way you can mess up password hashing and storage, no possibility of a password database being stolen, because you simply do not have passwords.

  4. Not having a password to enter the first time reduces friction for most users.

  5. In terms of user experience when coming back to a site, many people end up doing something similar to the above process anyway, because they forget their passwords. This is especially true for sites that people are not going to use very often — for example, a booking process for a conference that might happen once a year. In this case, people either:

    1. choose weak passwords that they can remember easily, which is bad for security,
    2. re-use a password so they can remember it easily, again bad for security, or,
    3. forget their password.

    However, with a password reset, the process is much, much worse:

    1. First they have to remember if they signed up for the site in the past, to work out if they should “log in” or “create account”.
    2. Then they have to make several attempts at remembering their password.
    3. Then they’ve got to use the password reset feature (hopefully it isn’t hidden, but I’ve seen users struggle with this when literally the only things on the page were the login form and a “Forgotten your password?” link).
    4. They then have to check their email and click the link.
    5. Now they have to negotiate a new password form, possibly including a strength monitor that won’t allow them to choose a weak password.
    6. Having finally set a new password, they now have to navigate to the login form again (because sites very rarely integrate password reset with log in, also usually for some good reasons), and re-type their email address (often for the 3rd time by now), and their password (again, typically for the 3rd time, not including all the failed password attempts).

    By removing the password entirely, most of these steps are eliminated. Steps 1, 2 and 3 are replaced by a single method for logging in — “Enter your email address”. Step 4 is the same, steps 5 and 6 are eliminated.

There are some additional advantages:

  • By doing email verification every time, we ensure that we still have a working email address. If we use some email/username + password combination for login, we have to add some kind of regular “Is this still your email address?” feature, or find ourselves unable to contact our users.

  • For any prompting or promotional emails that we send to a user, we can log them straight away in using this mechanism. As already discussed, this is not a reduction in security in the typical case. If we implement the system using a query string parameter containing a token and a generic middleware that checks the token, we can use this system on any page on the site with no extra work.

    So, for example, if we send an email asking for payment, the link can take them straight to the payment page, already logged in. This is the ideal situation, and we can do it with the tiniest amount of work (adding a query parameter to a URL in an email), because we can just re-use the existing login mechanism.

  • There are significant improvements for privacy concerns.

    A typical email + password login system has some problems when it comes to privacy, because it is often possible for an attacker to determine that a certain person has an account with a web site. This can be often be done from several pages on the site:

    1. The account creation form
    2. The log in form
    3. The password reset form

    And it can be done in a number of ways:

    1. By looking at the different error/validation messages that are returned by these pages, for the cases of existing or non-existing accounts.
    2. Even if the messages returned are identical, by doing timing attacks on the pages.

    Fixing method 1 often results in UX problems — e.g. if a user doesn’t have an account and is trying to log in, we can no longer tell a user that they don’t have an account and need to create one, we can only tell them their email/password combination is incorrect, and leave them to struggle. Similarly with password reset. Our user encountering scenario 5 above now feels like this:

    Man getting very mad with computer.

    Fixing method 2 can be very hard. The use of strong password hashing makes a timing attack on the login page trivial if no precautions are taken. Django, for instance, was vulnerable to this for a long while. It now has rudimentary mitigation, which fixes trivial attacks, but a complete fix is very hard. Making the code paths for “yes we found a user record” and “no we didn’t” take exactly the same amount of time would be very hard, and an attacker who was in the same data centre as your server (where network transit noise is much reduced) would probably not have a hard time doing a timing attack on the current code.

    However, with the system described in this post, these attacks, and the UX problems, are all completely mitigated. We send the verification email whether there is already an account or not, with exactly the same message (which doesn’t confuse the user), without looking up the account in the database first. We can check whether we need to create a new account or retrieve the old one when the email has been verified, so there is no timing attack possible on this part of the code.

  • On a code level, the amount of code required for this is very small. Compared to the typical alternative (email/username+password, all the forms to manage passwords, password reset etc.), it is tiny. That alone gives big maintenance and security advantages.

Disadvantages

There are of course some disadvantages:

  • Not all users have secure email systems, and emails could be intercepted, allowing an attacker to use someone else’s account. As noted already, you are already living with the same issue if you have a password-reset-via-email-link feature.

  • Users have to go through the “check your email” cycle every time they log in. For the kind of site that people are using daily, and if the login session is configured to expire relatively quickly, this will be annoying. But for use cases where users don’t visit the site often (e.g. occasional conference booking), this won’t be a problem.

  • If someone’s email address changes, this system has more problems, because in essence it uses an email address as the primary key for the account. To deal with this, you would need to store some other personal info or communication mechanism that could be used to verify the person is the same person, and then have some automatic or manual process for merging accounts etc.

    Alternatively, you can live with the fact that if their email address changes, they no longer have access to their old account. For the site I built (a booking system for yearly summer camps), this has not been a problem — it just means that people don’t have the shortcut of being able to re-use information from previous years.

Implementation issues

There are some implementation issues to be aware of, especially security related:

  1. You need a correct and secure way of creating the unique login links. They need to contain some kind of token that verifies an email address, a token which cannot be guessed by an attacker.

  2. The login links should expire — so that a temporary breach of someone’s email account, or accidentally sharing the link, doesn’t given an attacker login access forever.

  3. When comparing the token, you need to be aware of timing attacks.

  4. Security tokens in URLs are a dangerous thing, as they can easily be pinched. It can happen when a user copy-pastes or shares a URL, and it can happen if a page links to has any 3rd resources, which will then be able to see the URL (and the token) via the Referer header.

    Because of this, the token should be checked before any page is rendered, and you should redirect immediately, either to a failure page if it doesn’t match, or to a URL without the token. If you use a query string parameter for the token, this is easy — for the success case you just return HTTP redirect response to the same URL but without the token query parameter (and with a login cookie attached to the response).

  5. You should do case insensitive comparison on email address when looking for an existing account — people don’t always type their email addresses with the same case.

  6. As with all login mechanisms, you should give attention to providing a “log this account out on every device that is logged in to it”. This is often linked to a password change mechanism (which we don’t have). It can require additional work to ensure that we are removing a session, not just removing the session cookie from browser.

In my implementation, I use Django’s TimestampSigner to sign the email address. This takes care of 1 (Django uses a HMAC on the string), 2 (you just pass the max_age parameter to unsign) and 3 (Django’s Signer uses a constant_time_compare function internally).

I then base64 encode the result to produce a tidy URL. This results in a longish URL, but not too long to be impractical. I created a small class to wrap up the encoding and decoding.

(An alternative would be to create a nonce and store it in a database on the server, associated with the email address. The implementation above has the advantage that it doesn’t require server side resources, but the disadvantage of requiring a longer URL).

I do the checking in a middleware, including the redirect to handle item 4 above. I currently use Django’s signed cookies for implementing login. If a server side session was used, then it would be easier to implement “log out from all devices”.

I’m using a custom model for this account, which does not have a password field, and I’m also using a normal User model for other purposes, so it doesn’t make sense for me to release this as a standalone Django authentication library. But feel free to take the code and do so, or borrow in any other way.

There are other variations on this that could be used, but I think the basic pattern is very useful for some use cases, eliminating a lot of the user hassles and programmer headaches often found with passwords.

This is also not meant to be an alternative to things like OAuth. It is meant to be an alternative to email+password logins. If OAuth is used as well (should you venture down that somewhat dubious path, ), then enhancements are possible — for instance, for people who create accounts via OAuth, there could the option to disable login by email link. This would mitigate the risk of account takeover due to people with insecure email providers.


Updates:

  • 2016-07-18: Note about alternatives like OAuth2
  • 2016-07-18: Note about implementing “log out from all devices”
  • 2016-07-19: Note about security of email services.
  • 2016-07-19: Paragraph about prior art

(Thanks to reddit comments for the prompts that pointed some of these issues out).

Comments §

...loading...
blog comments powered by Disqus