Securing PHP User Authentication, Login, and Sessions

Many, if not all, of you have had to deal with creating a secure site login at some point in time. Although there are numerous articles written on the subject it is painstakingly difficult to find useful information from a single source. For this reason I will be discussing various techniques I have used or come across in the past for increasing session security to hinder both session hijacking and brute force password cracking using Rainbow tables or online tools such as GData. I use the word hinder due to the fact no foolproof methods exist for preventing session hijacking or brute force cracking, merely increasing degrees of difficulty. Choose a method wisely based on your site’s current or anticipated traffic, security concerns, and intended site usage. The following examples have been coded using PHP and MySQL. I more than willingly accept comments, suggestions, critiques, and code samples from readers like you as they benefit the community on the whole.

Update: Security Concerns with Hashing Algorithms

There are some inherent security considerations to take into account when using very fast hashing algorithms such as SHA or MD5. Modern day, multi-processor computers and GPUs can quickly brute-force passwords that aren’t encrypted with a very slow, secure algorithm. For these reasons, it is recommended you do not use these and instead use bcrypt encryption or sha-256/512 with key stretching. In the near future there will be a post containing this updated method for secure authentication.

The One-Way Password Hashing Algorithm

The password validation function

Below this line are older, obsolete methods for securing your data. I do not recommend using the password hashing methods outlined below, but the general ideas and concepts still apply.

Method 1: Hashed Password, Unique Salt

The first method involves storing a unique salt in one of your configuration files, a define statement, or class constant. Salting your passwords prior to hashing (MD5, SHA1, SHA256, Whirpool, etc.) hinders such attacks by increasing the amount of storage and computation required to crack your password. For a full listing of supported hashing algorithms, take a look at hash_algos. The primary concern with a singular unique salt for any number of stored passwords is that once figured out, the salt becomes utterly useless.

Part 1: The MySQL Table

Part 2: The One-Way Password Hashing Algorithm

Part 3: The password validation function

Method 2: Hashed Password, Random Salt

The second method utilizes a more secure form of salt, the random salt. The random salt is generated upon account creation and is unique to that account. The benefit of using random salts is that a compromised account will have no adverse effect on the remainder of accounts due to the uniqueness of the salts on a per user basis. A good salt should incorporate letters, numbers, and symbols and preferably be over 8 characters in length.

Although this method is still more secure than the first, it may have a negative impact of requiring you to perform an extra database query to grab secure data after validating the session. The first query performs a lookup of the uid, password, and salt based on either a username or email address. The second query would generally be performed if you required further data that was not contained within your user login table. This query would more likely utilize a join on related related tables to gather further information. Benefits of utilizing a random salt

Part 1: The MySQL Table

Part 2: The pseudo-random salt generator

Part 3: The One-Way Password Hashing Algorithm

Part 4: The Password Validation Function

Method 3: Hashed Password, UNIX Timestamp Based Salt

Assuming your database becomes compromised, it would be very easy for an attacker to piece together your encryption algorithm if your user login table contains a field named salt. This method provides an abstracted way of hashing your password based on a substring of the last modified date of your user login entry. You may consider storing your user’s created date as a TIMESTAMP as opposed to an UNSIGNED INT because of the Year 2037 Bug. This may seem a little ridiculous to assume my PHP scripts will still be running in 2037, but then again nobody readily anticipated the Y2K Bug either. The benefit of using a pre-existing, non-changing field in your user table as a salt is that it adds obscurity. If your database is compromised, it is not readily apparent that your passwords are salted and there is no indication of how they were salted. If you do choose to go the UNSIGNED INT route with your user’s createed date, I recommend doing some obfuscation of the date as your salt (i.e. using every odd digit, reversing the integer, etc.).

Part 1: The MySQL Table

Part 2: The One-Way Password Hashing Algorithm

Part 3: The Password Validation Function

IP Address Validation

IP address validation relies on the fact that the majority of users will maintain a static IP over the duration of their site visit. On each page load we would be performing a comparison for equality on the visitor’s current IP and the stored copy of their IP in the session. Drawbacks from this method are related to the fact that many individuals do not have a static IP. Some ISP providers lease IP addresses for a given amount of time before expiration (generally for the duration of their online session) while others may utilize numerous proxies.

One solution you can implement to alleviate the static IP issue is to take a substring of the IP address and use the the first 3/4 of an IPv4 address (24 bits / 3 bytes / third octet). If validation fails utilizing this method logout will be enforced and the user will be required to log back in, thereby updating their IP address. Please note that this method may be bypassed by IP spoofing.

Obtain the user’s IP address and run it through the trimIP() function on page load.

Check the IP address against a session variable, i.e. $_SESSION[‘user_ip’], to see if they match.

If the new IP address does not match the session IP and the session IP address is not empty, invalidate the current logged in user’s session.

For the sake of not leaving anything out, here’s a proof of concept for validating IP addresses:

User Agent Validation

Many individuals prefer validating the user’s browser agent as opposed to their IP address because the information should remain static over the duration of the site visit. There exist a plethora of different user agent strings depending on both your browser, operating system, and versioning. An example user agent would be:

Since user agents are browser dependent and a user’s session is only valid within their currently opened browser, validating the user agent is a great way to enhance security of your site. Below is a quick proof of concept implementation on how to go about adding a user agent validation check to your existing session handling:

As a wrap up, I would ultimately recommend incorporating both user agent validation as well as IP address validation into my session/authentication handling.

Corey Ballou is the CEO of POP.co. Whether you're a student, young professional, entrepreneur, startup, or small business, you can be up and online fast with your own custom domain, email, and webpage on POP.
Corey is a professional PHP developer by trade, specializing in custom web applications development for startups, small businesses, and agencies.
Follow Corey on Twitter @cballou.

NooB question: How does one get the salt from the top example when sending the password to the create_hash function?

Tiago Leonardo Costa Dias

since define (in PHP) is the creation runtime constant, saying that, it’s an value that you can use in any part of the script.
to validate if a constant (created by define function) is created, you use defined(“constant_name_like_this”)
to get the value from it, first of all create it using define(“some_awesome_name”,”value”) or define(“some_awesome_name”,true) or etc…http://php.net/manual/en/function.define.php will give some usage example and maybe some other explanation.
since the salt value is stored in a constant, you can access it’s value from calling it by it’s name. like:
<?php
define('awesomesalt','uniquesalt'); // creates an constant named as awesomesalt
function encript(){
// […] some code here
$saltvalue=awesomesalt; //gets the value from the constant awesomesalt
somefunction(awesomesalt); //the value parsed is the awesomesalt value

bernard

in your fallback, do you really need to loop 20000 times, hashing the same string 20000 times… seems like 2 or 3 times seems more then sufficient to me… it s a fallback and hopefully barely executed 😉 lol

http://www.coreyballou.co/ Corey Ballou

Hey Bernard, the reason for the high number of iterations is due to the extremely quick hashing of sha512. The quicker the hash, the easier it is to brute force. When it comes to encryption and logins, you want things to perform relatively slow so they can’t be brute forced.

bernard

not to mentioning $hash always seems to be empty and by result is not impacting the end result in any way… Sorry , didn’t look at the rest of the code as I was shocked of what I was seeing in the first few lines …

Cedric

Hi Corey,

First of all, thanks for putting this info into 1 article. As you stated, it’s a pain in the *** to find it all documented in one place.

That being said there’s a few things that come to mind.

When it comes to IP-checking for session validation, how would you improve accessibility for users with Dynamic IP’s or users who are behind proxies?
Let’s say I’d develop an application that mostly targets professional users, who are supposedly behind a proxy or firewall, it would be impossible to use this method since there’s no way to determine if the IP that is connecting is actually the person that USED to be behind this IP and vice versa (the same user could have a different IP).

As for user agent-checking, it’s a nice barrier for someone who doesn’t really care, but for a descent attacker who really wants to get in, there would be no difficulties in intercepting the User agent and Spoofing it (isn’t it even sent in the HTTP-request?).

I’ve been cracking my skull to find workarounds for the 2 points mentioned above, but somehow it seems that it’s quite difficult if not impossible to improve both security and accessibility when working PHP sessions.

http://seapip.com/ Thomas Gladdines

What about password_hash and password_verify in php?

Edmundo Sanchez

There are some missing parenthesis :(, I got it working and also am getting the salt from the create_hash method so I can store it. Wonderful material.