This is going to take a few postings to complete because of the character count limit. Plus there's quite a bit of information involved, and I'm a horribly slow typist, so please be patient.
What you are looking for is very common indeed, and it's almost a blessing that you don't know the magic incantation to Google up a quick solution. You would think that because user logins are so common that it would be a solved problem, but it's unlikely that that will ever actually be the case. The best we can ever do is "good enough for now" -- and most of the solutions you will find on the internet weren't really good enough for 1995. Frankly, most of the solutions
in production on web sites that absolutely need to be secure are nowhere near good enough for use on a simple hobby site, let alone for your online banking account. So let's put you in the above-average group, and I'll start with a bit of explanation.
First, you must never know your users' passwords. If you store the passwords, even if you store them encrypted ("encrypted" means that they can be "decrypted"), then you've lost the game before it even starts. That means that you can never tell the users their passwords if they forget them, of course, but it also means that a hacker can't find a way to grab your database and get a complete list of user names, email addresses and passwords. So what? Your site isn't really all that important. Perhaps not, but people tend to use the same one or two passwords all over the internet, so a hacker that manages to get your database of users probably has their email accounts, online bank accounts, and so forth. If you get it wrong, it puts your users at a lot of risk.
Passwords should be stored using a one-way "hash" function. The same data will always result in the same value using the same hash algorithm, but there's no way to work backwards from the hash value to the original data -- you have to use brute force, trying different passwords until you come up with a match. There is a problem with using a simple hash, though -- someone can use a "rainbow table", which is basically a pre-calculated database of passwords and their hashes, if they know the hash algorithm you're using. The most common "secure" (take that with a grain of salt) login scripts on the web use a simple MD5 hash algorithm, and comprehensive rainbow tables for MD5 have been available in the darker corners of the web for a decade or more.
That "grain of salt" is part of the key to making it better. A "salt" is an additional value you add on to the user's password before hashing. It makes rainbow tables useless, especially if you use a different salt (nonce) value for every user. You don't need to get too very sophisticated with the salt; I usually use an MD5-hashed version of the time at which the account was created. An MD5 hash is 32 characters long, so you'll need a char(32) not nullable column in your login table to store the salt.
---------- Post added at 07:20 AM ---------- Previous post was at 07:20 AM ----------
(Part II)
That leaves only one major outstanding problem: hash functions are too darned efficient these days. If you are using even one of the better hash functions (like SHA256), and your user database goes astray, it's only a matter of hours before the bad guys have your users' passwords. Hours are not good enough; you need to make it into months or years. That's where we make a deliberate decision to put our usual love for efficiency and speed aside and deliberately choose to use a slow and awkward method. Probably the best method to use is something called
bcrypt, but that relies on your host having a certain PHP extension installed, so it's not always something you have access to. Luckily, there is something almost as slow and ugly as bcrypt that you can almost always rely on being able to use, and that's
PBKDF2 (Password-based Key Derivation Function 2, which is an IETF standard: RFC 2898):
PHP:
<?php
// PBKDF2 Implementation (described in RFC 2898)
function pbkdf2($password, $salt, $iter_count=1000, $key_length=256, $algorithm = 'sha256', $start_pos=0)
{
$key_blocks = $start_pos + $key_length;
$derived_key = '';
for ($block=1; $block<=$key_blocks; $block++)
{
$iterated_block = $current_hash = hash_hmac($algorithm, $salt . pack('N', $block), $password, true);
for ($i=1; $i<$iter_count; $i++)
{
$iterated_block ^= ($current_hash = hash_hmac($algorithm, $current_hash, $password, true));
}
$derived_key .= $iterated_block;
}
return substr($derived_key, $start_pos, $key_length);
}
?>
As written here, it uses the SHA256 hash algorithm (unless you tell it to do otherwise) a thousand times (again, unless you feed it a different, and preferably larger, number to use), so that few hours it would otherwise take to get your users' passwords has turned into months at best. By default, it returns a 256-character string as the "key". That sounds like a lot, but one of the vulnerabilities of a hash algorithm is something called a "collision", the possibility that two different inputs can result in the same output. If database space is at a premium, you can reduce the
$key_length value, but it really takes an awful lot of users before that should be a problem, so I'd suggest leaving it alone. That means you need a char(256) not nullable column to store the password key.
While it is okay to enforce a minimum length for a password for the users' own protection, that should really be the only thing you enforce. If the users can't remember their passwords because of your rules, they're likely to write them down somewhere, and that becomes its own vulnerability. Again, since users tend to use the same password in multiple places, you may be exposing them to danger elsewhere. And since you're storing exactly 256 characters of gobbledegook no matter what their password is, if they want to use the first chapter of
Harry Potter and the Half Blood Prince as their password, it's no skin off of your nose. You still check
pbkdf2($password, $salt) against what was stored in the database.
Your user table will also need a column for the username (a varchar(32) should do, but you can make it longer if people are using their formal names rather than "handles") and an email address (this can be long, so a varchar(256) might be in order). Both of those columns should enforce unique values and should not be nullable. You are probably going to want the users to be able to change their usernames and email addresses at some point, but you'll still want to be able to connect the user to other data you're storing elsewhere (profile info, avatars, that sort of thing), so you'll also want to have a user_id column, which can be a number that auto-increments with each account created.
There are still a couple of columns to go in the login table. You'll want a status column to indicate when there is a pending action (like an account confirmation or a password reset) or if the account has been banned/closed. That can be an integer. You'll also need a column to store the key for a pending action -- that will be part of the URL of the link you send to the user to confirm their account or to reset their password. I'd suggest
storing the key using the pbkdf2 function as well, just in case, so the column would be a nullable char(256).