The best think is to not reinvent the wheel. But, I understand, in PHP world it may be difficult to find a high quality component which already does that (even I'm pretty sure that the frameworks implement such things and their implementations are already tested, solid, code-reviewed, etc.)
If, for some reasons, you can't use a framework, here's some suggestions:
Security related suggestions
Use PBKDF2 or Bcrypt if you can. It's done for that.
Rationale: both algorithms can make the hashing process arbitrarily slow, which is exactly what you want when hashing passwords (quicker alternatives mean easier brute force). Ideally, you should adjust the parameters so that the process becomes slower and slower over time on same hardware, while new, faster hardware is released.
If you can't, at least don't use MD5/SHA1. Never. Forget about it. Use SHA512 instead, for example. Use salt too.
Rationale: MD5 and SHA1 are too fast. If the attacker has access to your database containing the hashes and has a (not even particularly) powerful machine, brute-forcing a password is fast and easy. If there are no salts, the chances that the attacker finds the actual password increases (which could make additional harm if the password was reused somewhere else).
In PHP 5.5.0 and later, use
Rationale: calling a function provided by the framework is easy, so the risk of making a mistake is reduced. With those two functions, you don't have to think about different parameters such as the hash. The first function returns a single string which can then be stored in the database. The second function uses this string for password verification.
Protect yourself from brute force. If the user submits a wrong password when she already submitted another wrong password 0.01 seconds ago, it's a good reason to block it. While human beings can type fast, they probably can't be that fast.
Another protection would be to set a per-hour failures limit. If the user submitted 3600 wrong passwords in an hour, 1 password per second, it's hard to believe that this is a legitimate user.
Rationale: if your passwords are hashed in an insecure way, brute force can be very effective. If passwords are stored safely, brute force is still wasting your server's resources and network bandwidth, causing lower performance for legitimate users. Brute force detection is not easy to develop and to get right, but for any but tiny system, it's totally worth it.
Don't ask your users to change their passwords every four weeks. This is extremely annoying and decreases security, since it encourages post-it-based security.
Rationale: the idea that forcing passwords to change every n weeks protects the system from brute force is wrong. Brute force attacks are usually successful within seconds, minutes, hours or days, which makes monthly password changes irrelevant. On the other hand, users are bad at remembering passwords. If, moreover, they need to change them, they will either attempt to use very simple passwords or just note their passwords on post-its.
Audit everything, every time. Store logons, but never store passwords in audit log. Make sure that the audit log cannot be modified (i.e. you can add data at the end, but not modify the existing data). Make sure that audit logs are subject to regular backup. Ideally, logs should be stored on a dedicated server with very restrictive accesses: if another server is hacked, the attacker will be unable to wipe out the logs to hide his presence (and the path taken during the attack).
Don't remember user credential in cookies, unless the user asks to do it (“Remember me” check box must be unchecked by default to avoid human error).
Ease of use suggestions
- Let the user remember the password if she wants to, even if most browsers are already this feature.
- Don't use Google approach when instead of asking for user name and password, the user is asked sometimes for password only, the user name being already displayed in a
<span/>. Browsers can't fill the password field in this case (at least Firefox cannot do that), so it forces to logoff, then to logon with an ordinary form, filled by the browser.
- Let the user enter either her user name or her mail address. When I register, sometimes the user name I want to enter is already taken, so I must invent a new one. I have all chances to forget this name in two hours.
- Always keep a link to "Forgot my password" feature near the logon form. Don't display it only when the user failed to log on: the user who don't remember her password at all have no idea that she must submit a wrong one in order to see the "Forgot my password" link.
- Don't use security by post-it.
- Don't invent stupid rules related to the password in order to make it weaker. Example: "Your password must start with a lowercase letter."; "Your password cannot contain spaces."