Many sites have features that are only accessible after registering a user account on them. These sites include forums, on-line games, corporate intranets and non-public utility sites amongst others.
These sites are typically secured through a system whereby a user chooses both a unique user name and a password that will be used to authenticate them.
The system I propose here is more convenient for users, and in many cases more secure by removing the requirement for the user to choose or remember a password.
A typical sequence for registering and logging on to a web site forum might be the following:
The most obvious thing that the system has going for it is that site administrators & developers and end-users are familiar with it.
The biggest problem is the cost in managing the passwords. Users lose and forget them and many sites fail to either salt and hash, or encrypt the passwords leaving them vulnerable to breaches caused by database theft.
Many users also choose very weak passwords and often re-use the same password for multiple web sites.
If a password is discovered by a third party then the end-user is often unaware of the breach.
This is the proposed system as an end user sees it.
From a user's perspective the system appears simpler and there is no password to remember. The account however becomes tied to the browser that they are used when they followed the activation link¹ [1For some use cases this is an advantage, for example access to extranets can be tied to a business computer. Simple implementations would limit a user to a single browser at a time, but it is possible to allow multiple browsers.].
The implementation outlined here is not suitable for real-world use without incorporating at least some of the increased measured described later. It is intended as a simple introduction to the ideas.
There are five basic operations that are required:
The very simple implementation outlined here is open to several exploits which are addressed in the next section, Increasing the security.
In pseudo-code the registration process is this:
function register( name, email ) { if ( FindUserByEmail( Input( email ) ) ) throw "It looks like you're already registered"; if ( FindUserByName( Input( name ) ) ) throw "Please choose a different user name"; var user = new User(); user.name = name; user.email = email; issue_token( user ); }
If the user changes browser, or the token attached to their browser expires then a new token must be issued.
function issue_token( user ) {; user.lives = 1; user.token = random_hash(); user.token.expires = now() + 90; EmailToken( user ); }
lives
tells us how many browsers can be activated with the same token.The activation process sets a cookie on the user's browser. Again in pseudo-code:
function activate_browser( user, token ) { if ( !user ) throw "An error occurred during activation"; if ( user.lives == 0 ) throw "An error occurred during activation"; if ( user.token != token ) throw "An error occurred during activation"; if ( user.token.expires < now() ) throw "An error occurred during activation"; user.lives = 0; SetCookie( "name", user.name, user.token.expires ); SetCookie( "token", user.token, user.token.expires ); }
The final part is the authentication.:
function authenticate() { if ( FindUserByName( GetCookie( name ) ) ) throw "Not authenticated"; if ( !user ) throw "Not authenticated"; if ( user.lives != 0 ) throw "Not authenticated"; if ( user.token != GetCookie( "token" ) ) throw "Not authenticated"; if ( user.token.expires < now() ) throw "Not authenticated"; }
lives
are checked in order to ensure that the account has been activated.A user needs to be able to choose to immediately revoke their authentication, for example by logging off from a site. This will stop any cookie recovered from a computer or copied by a third-party from working.
function authenticate( user ) { if ( !user ) throw "Not a user"; user.token.expires = now(); }
There are a few simple measures that can be taken that further increase the security of the system.
Storing the token expiration on the server immediately stops the use of stale cookies, but a further improvement can be gained through allowing a shorter time frame for the use of the activation link in the activation email.
The following pseudo-code allows the activated browser to be used for 90 days (as before), but the activation email must be used within two days.
function issue_token( user ) {; user.lives = 1; user.token = random_hash(); user.token.expires = now() + 2; EmailToken( user ); } function activate_browser( user, token ) { if ( !user ) throw "An error occurred during activation"; if ( user.lives == 0 ) throw "An error occurred during activation"; if ( user.token != token ) throw "An error occurred during activation"; if ( user.token.expires < now() ) throw "An error occurred during activation"; user.lives = 0; user.token.expires = now() + 90; SetCookie( "name", user.name, user.token.expires ); SetCookie( "token", user.token, user.token.expires ); }
The email activation code is now only useful for a very short window, but the browser activation is long enough lived not to be too painful for the user.
The simple implementation allows the cookie content to be built directly from the contents of the activation email. This can be stopped through generating a new token on activation and using that instead from that point forwards. Combining this with the shorter expiration of the email token gives the following:
function issue_token( user ) {; user.lives = 1; user.token = random_hash(); user.token.expires = now() + 2; EmailToken( user ); } function activate_browser( user, token ) { if ( !user ) throw "An error occurred during activation"; if ( user.lives == 0 ) throw "An error occurred during activation"; if ( user.token != token ) throw "An error occurred during activation"; if ( user.token.expires < now() ) throw "An error occurred during activation"; user.lives = 0; user.token.expires = now() + 90; user.token = random_hash(); SetCookie( "name", user.name, user.token.expires ); SetCookie( "token", user.token, user.token.expires ); }
Even though tokens must be sent via email it is still not a requirement that the email address be stored on the server, at least not in a readable format.
If no email address is stored on the server then a malicious user could input somebody else's user name and their own email address and have a token sent to them. To stop this the system must ensure that the activation token is sent to the right email address, but this can be done by storing a hash of the original email address:
function register( name, email ) { if ( FindUserByEmail( Input( hash( email ) ) ) ) throw "It looks like you're already registered"; if ( FindUserByName( Input( name ) ) ) throw "Please choose a different user name"; var user = new User(); user.name = name; user.email = hash( email ); issue_token( user, email ); } function issue_token( user, email ) {; user.lives = 1; user.token = random_hash(); user.token.expires = now() + 2; EmailToken( user, email ); }
A malicious user who manages to gain access to a copy of the database would be able to build cookies allowing them to masquerade as any user with an active token. This risk can also be mitigated by changing the activation and authentication processes by salting and hashing the tokens stored in the database.
function activate_browser( user, token ) { if ( !user ) throw "An error occurred during activation"; if ( user.lives == 0 ) throw "An error occurred during activation"; if ( user.token.expires < now() ) throw "An error occurred during activation"; var salt = random_hash(); user.lives = 0; user.token.expires = now() + 90; user.token = hjash( random_hash() + salt ); SetCookie( "name", user.name, user.token.expires ); SetCookie( "token", user.token, user.token.expires ); SetCookie( "salt", salt, user.token.expires ); } function authenticate() { var user = FindUserByName( GetCookie( name ) ); if ( !user ) throw "Not authenticated"; if ( user.lives != 0 ) throw "Not authenticated"; if ( user.token != hash( GetCookie( "token" ) + GetCookie( "salt" ) ) ) throw "Not authenticated"; if ( user.token.expires < now() ) throw "Not authenticated"; }
Up until now it is assumed that the authentication token is distributed by email. This is of course not a requirement. Other distribution methods might include:
This system is still open to a number of vulnerabilities, many that are common to all web sites due to the nature of HTTP (for example man-in-the-middle attacks) and it is also open to a number of vulnerabilities that normal forms-based password authentication is open to.
Like systems that email the password to the user, any malicious user who gains access to the email potentially gains access to the user's account.
For password based systems though this access will normally go undetected by the user. Using this password-less system however the attacker cannot gain long term access to the system without quickly alerting the user.
If the attacker steals the user's email the only window of opportunity open to them is between the activation email being sent out and the proper using requesting a replacement activation code as the replacement immediately invalidates the attackers token.
Like all cookie based authentication systems this one is also vulnerable to the theft of the cookie from a user's computer. There are a number of ways to mitigate some aspects of this:
None of these are ideal, but each make the job of the attacker a little harder.
One attack that this system is vulnerable to that a password based system is not is a denial of service attack where a malicious user keeps requesting new authorization tokens for another user. In order to do this they must guess the email address that the system has for the other user, often an easy task.
A number of other techniques can be used to mitigate or eliminate the threat depending on the exact security requirements of the site employing password-less authentication.
The simplest way to defeat this attack is to require a separate confirmation (via email) that the reset should occur if a reset has been carried out recently.
Malicious users can of course try to gain access to the mailbox that the authentication tokens are being sent to. This is no worse than a compromised mailbox where password reminders are sent. On this system however an attacker cannot make use of the authentication tokens without making that use apparent to the mailbox owner.
If the mailbox has been abandoned then an attacker can take over both the mailbox and any other accounts that send authentication tokens to it. Again though this is the same as for sites that email password.
Implementing this system so that a user is able to have several browsers authenticated at the same time is more complex to implement and get right.
A naïve implementation (for example simply using a higher number than one for the user's lives) would be incompatible with most of the features to improve the security of the system over the simplest implementation first outlined.
Most web based authentication systems already capture email addresses and will send emails containing password reminders. Because of this the sending of activation tokens in this way does not impose any security vulnerabilities not already present in most web-based authentication protocols.
It should however be easier for many users and has the potential for improved security for most users as well. It is also no harder to implement than a properly implemented password-based authentication system.
I believe that at worst this system is no worse than most password based authentication systems, and at best is more secure.
The good parts of the plan are probably copied from other people. The stupid bits are mine.