The system I have been building is a direct marketing system (don’t hate me, it’s opt-in) and to be compliant with their ethics policy it requires “2 click unsubscribe” functionality from all its email campaigns; 1 click on the link in the email, and 1 click on a big “Don’t Ever Send Me Email Again” button. So people actually have an option to opt out at any time, as any good (and I use that word in the relative sense) email marketing system should.
The problem this poses for me though, is how can I let a member unsubscribe without authenticating themselves? Authentication would take more than 2 clicks! The obvious solution is to not require authentication by simply adding the members identifier (email address, user name, database id, whatever can identify them uniquely) to the link. So I would have a URL something like:
http://example.com/member/unsubscribe?member=1234
And simply make all the options on that page act on the member identified by the identifier. Great! Until a malicious netizen comes along, seeing the scheme decides to systematically unsubscribe all my members. This would be achieved easily by guessing the identifiers, made even easier because they’re sequential, and clicking the unsubscribe button.
The initial solution is to use some other “key” in the link, one that is not so easy to guess. So a key was added, giving the url a form like:
http://example.com/member/unsubscribe?member=1234&key=SECRETKEY
Were SECRETKEY is a key they is generated randomly when the user signs up and stored with their account details. Using this, the system would allow a user to log in simply by clicking the link, they could then unsubscribe.
The problem being, of course, that by simply logging the user into their account anyone who has the link has full access to the account without needing to know any more information. So they could then read (and change) email addresses, passwords etc. Not ideal.
My solution has been to take the key a step further, and limit it to only the intended page. In this case the unsubscribe page. So my new url looks like this:
http://example.com/member/unsubscribe?token=1234:HASH
I’ve removed the member variable and combined it into a delimited token. This was mainly because I thought it looked a little nicer, it would function just as well as a separate variable. The real change is in the HASH. The new hash still uses the SECRETKEY that was used in the previous iteration, except it is now combined with the url that I want to give them access to, and an added salt so all tokens can be invalidated if required. In PHP, this looks something like this:
<code>$allowedUrl = '/member/unsubscribe';
$hash = md5($salt . $allowedUrl . ':' . $secretKey);
$token = $id . ':' . $hash;
</code>
On the page, to allow access, the token is decomposed, the member’s identifier extracted, and their key retrieved. Their key is then used in the same process to generate a hash for the URL they are requesting. If the hashes match, they have access. To make it a little more user friendly, the allowed URL is added to their session, so they still have access to the page even if they lose the token from the URL. If they hit any other page with the restricted URL in their session they are logged out and sent back to re-authenticate.
One shortfall that I am aware of, and that was pointed out in a discussion on #phpc, is the members secret keys should ideally be rotated, so it can only be used once. This would mean that stealing a link would at worst allow one login, and only to one page. This may be implemented if it doesn’t impact too much on usability (that classic trade-off, security vs. convenience).
Now, I am not a security expert and as such don’t recommend anyone take my advice (is this good enough for a disclaimer), but apart from the one caveat mentioned, I think this solution meets the requirements without forfeiting too much in security. Any comments or advice on the technique are, as always, appreciated.