views:

359

answers:

5

Hi

I'm creating a PHP website which involves users signing up, and I'm wondering about best practices for "email confirmation" codes.

New users must confirm their email addresses - I do this by generating a code and sending it to the user in an email, which he can then use to activate his account. Rather than storing this key in a database, I'm using a handy little workaround: the code is the result of:

md5("xxxxxxxxx".$username."xxxxxxxxxxx".$timestamp."xxxxxxxxx");

Where $timestamp refers to the user-creation time. On the whole I was quite pleased with this, but then I got to thinking, is this secure enough? And what about the possibility of collisions? And I also need to generate codes for password reset, etc. If I used a similar methodology, a collision could result in one user inadvertently resetting another user's password. And that's no good.

So how do you do these things? My thoughts was a table of the following format:

codePK (int, a-I), userID (int), type (int), code (varchar 32), date (timestamp)

Where 'type' would be 1, 2 or 3 meaning "activation", "email change" or "password reset". Is this a good way of doing it? Do you have a better way?

Using a method similar to the above, could I automatically delete anything over two days old without using cron-jobs? My host (nearlyfreespeech.net) does not support them. If at all possible I'd like to avoid having a cron-job on an external host which wget's a script which deletes things, as that's just messy =P.

Thanks!
Mala

Update:
To clarify: I've realized the only way to securely and safely go about this task is by using a database, which is what the original function was trying to avoid. My question is on how the table (or tables?) should be structured. Somebody suggested I do away with codePK and just make the code a PK. So in short, my question is: is this what you do?

A: 

It was secure enough right up until the point where you published your method on the Internet! This is because you were relying on security by obscurity, which is not a good idea.

You ought to use some sort of keyed hash function or MAC ideally, which incorporates a secret key known only to you.

David M
obviously the 'x'es are not actually 'x'es in the code
Mala
In that case, you have created a keyed hash function and you're good to go. I should probably have guessed that... ;)
David M
;) sorry i thought I had specified that - but apparently I edited the bit where I explicitly said that out... in any case I'm wondering if the table structure / using a single table for all codes is appropriate
Mala
A: 

Why not make code field unique index? So there will never be collsisions?

Also, if you don't need to create hash from user input and match it to database hash (email confirmation, password reset etc) - you can add random string to the hash body, like md5('xxx'.$username.'xxx'.time().'xxx'.rand())

habicht
then i would not be able to check it without using a database - the whole point of that function was to avoid using them. If I'm going to be using a database anyway, then I can let the whole string be entirely random
Mala
+1  A: 

Why use any of the user's data as the basis for the authorisation key?

I presume you're storing the de-activated data in a database so why not just add an additional record that is simply a random key (perhaps an md5'ed uniqid with some additional manipulation) and then check against that?

middaparka
Originally I was trying to avoid having to use a database for this - hence the using userdata
Mala
To be honest, I can't help but think a database is the way to go, otherwise you're going to be hashing/manipulating a pretty limited pool of static data.
middaparka
A: 

Why not ask the user to enter their username and their code, thereby eliminating any problem with collision? You don't lose anything security-wise as you're still asking for the key which they'd get from the email, but you'd stop them being able to reset other users' passwords.

George Bashi
Ideally they could just click a link in an email. I don't want this to be horrendously long =P
Mala
+1  A: 

When I need these kinds of tricks it is normally of one of two reasons, both mentioned by you:

  1. As a key used for verification emails sent to the user
  2. As a key used for password-reset links

Of course there would be numerous other occasions where you would consider using such a construction.

First of all, you should always use some kind of salt that is hidden and that only you know. Note that this salt should be different for each user. The salt could for example be calculated as sha256(something random). This salt should then be stored in the database along with the username and password (hashed with the salt).

What I would do when sending the password reset link is to create another salt (don't give the user access to anything hashed with your salt. He knows his password, so using bruteforce he could potentially figure out your salt). The other salt, which essentially is only a hash of a random string (you might wanna go for md5 here, as you mentioned that the length is an issue), should you then save into your database.

Often you can just get away with adding an additional column to your users table. This, however, also has a few problems, mainly that once the password has been reset or the user has been activated, you will remove the key from the database, which results in most rows having null values, which in turn brings some other trouble.

What this essentially boils down to:

  • Hash your users' passwords using a unique-for-the-user salt (and perhaps a global, secret salt).
  • Generate a key by hashing a number of random or pseudorandom sources like timestamps, mt_rand() or even random.org if you really want random stuff.
  • Never use your global salt or the salt that is unique to the user for hashing anything that the user gets access to, including password reset keys, activation keys, etc.

Please not that I am by no means a security expert, and I have probably forgotten a number of things, and I may have mentioned some very bad practice things. Just my 5 cents ;-)

phidah