2016-07-17

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.

Rationale

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.

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.

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.

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

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:

choose weak passwords that they can remember easily, which is bad for security,

re-use a password so they can remember it easily, again bad for security, or,

forget their password.

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

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”.

Then they have to make several attempts at remembering their password.

Then they’ve got to use the password reset feature (hopefully it isn’t
hidden, but I’ve seen users struggle with this when the only thing on the
page was a login form and a “Forgotten your password?” link).

They then have to check their email and click the link.

Now they have to negotiate a new password form, possibly including
a strength monitor that won’t allow them to choose a weak password.

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:

The account creation form

The log in form

The password reset form

And it can be done in a number of ways:

By looking at the different error/validation messages that are returned by
these pages, for the cases of existing or non-existing accounts.

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:



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 or
privacy 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:

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:

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.

The login links should expire — so that a temporary breach of someone’s email
account doesn’t given an attacker login access forever.

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

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).

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.

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.

I do the checking in a middleware,
including the redirect to handle item 4 above. I use Django’s signed cookies
for implementing login result, but the session would be just as good (apart from
requiring more server side resources).

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 (e.g. ways to extend the
session without checking email again), 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.

Show more