As a prelude to creating our own authentication system, we need to create a session ID that a cookie will be set to. The purpose of the session ID is to uniquely identify you from all the other visitors to a site. Once the browser can identify you, state can be stored in a database or files. There are several ways we can employ to make a session ID:
exec("uuidgen", $sid);
This will set the string $sid with the output of the program uuidgen which generates a Universally Unique IDentifier, which is supposedly guaranteed to be unique in all of time and space across the entire universe. It might be more than we require for our purposes however. It also requires that there be a uuidgen program installed. UUIDs are about 128 bits of randomness, but the bits are encoded as a hex string with dashes, and thus could be smaller.
$sid
uuidgen
To generate a session ID ourselves we can try:
<?php function createSID() { // 6 bits worth of character data: $cookie_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-@"; $len = strlen($cookie_chars)-1; $sid = ""; // Loop to generate 126 bits of randomness as a string: for ($i = 0; $i < 21; $i++) $sid .= $cookie_chars[rand(0, $len)]; return $sid; } ?>
To keep track of login sessions, we need to store the session ID, perhaps along with a login name or other identifying information. What follows is a method I created to do ISU AD and local CS authentication.
First we create a session table. We assume that username is the identifier we are most interested in with respect to maintaining state between the server and the browser. Thus it should be possible for there to be multiple sessions for the same username (the same login being accessed from different computers.)
username
DROP TABLE IF EXISTS `session`; CREATE TABLE `session` ( `sid` varchar(128) NOT NULL DEFAULT '', `webkey` varchar(64) NOT NULL DEFAULT '', `username` varchar(64) NOT NULL DEFAULT '', `domain` ENUM('ISU','CS') NOT NULL DEFAULT 'ISU', `usrinfo` varchar(1024) NOT NULL DEFAULT '', `ipaddr` char(16) NOT NULL DEFAULT '0.0.0.0', `created` timestamp DEFAULT CURRENT_TIMESTAMP(), `expire` bigint(24) NOT NULL DEFAULT 0, `atime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY ( `sid` ) );
The purpose of the columns:
sid
webkey
domain
usrinfo
ipaddr
created
expire
atime
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `uid` int(10) NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL DEFAULT '', `password` varchar(256) NOT NULL DEFAULT 'x', `usrinfo` text NOT NULL DEFAULT '', `created` timestamp DEFAULT CURRENT_TIMESTAMP(), `atime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY ( `uid` ), INDEX `username` ( `username` ) ) AUTO_INCREMENT=1000;
A uid is often a good way to keep track of a user entry, as it is usually faster to lookup a particular user numerically rather than doing string comparisons against a username. Note how the auto increment is set to start at 1000, leaving room for low UIDs that could be used to indicate administrator/ privileged accounts.
The password field contains a hashed password, it should never contain a plain-text password. History is replete with a multitude of user databases that have been made public, always assume that yours will be next.
The usrinfo field is a JSON encoded field containing any additional user information that we might want to contain. Only base information or things that would be changed often (such as atime) really ought to be in the table itself, as tables are troublesome to add or subtract from, but a JSON object is a bit more flexible. Information that could be in usrinfo could include their real name, email address, access levels, etc.
The created and atime fields are the same as for a session, but with respect to the user account itself.
Passwords should be cryptographically hashed and salted when stored to prevent an attacker from gaining immediate access to someones password which may allow them access to other sites.
A salt is a random string that is "added" to a password to make it more difficult to discover the real password. Each password should have its own randomly generated salt.
A hash might be further perturbed by adding a cost function, i.e. the hash is fed into itself X number of times, often called "rounds".) Making the hashing much slower and computationally expensive.
Currently good algorithms might be blowfish (bcrypt) or sha256 or above is recommended. DES, MD5 and SHA1 should be considered unsafe and deprecated.
A hashed password might appear as:
$algorithm #$rounds=5000$somesalt$hashed password
$6$rounds=5000$somesalt$YID3kajQz9xL19czLNmg1Lt2JwM7Yt9PTP.tUPymFJckv/f9.jgKfR3YB1.nl//PtMT/EKyHH5iSLHx9V03W5.
generated with:
php -r 'echo crypt("gigo","\$6\$rounds=5000\$somesalt") ."\n" ; '
Outputs a SHA512 (algorithm #6) hash using "somesalt" for the salt with 5000 rounds.
Note that the $'s need to be escaped or single quoted to prevent PHP from trying to interpret them as variables for expansion.
In php we can use the crypt() or password_hash() (PHP >=5.5.0) functions:
crypt(string $password [, string $salt]);
password_hash(string $password, PASSWORD_DEFAULT);