Granted, there are some really good authentication systems out there and I sincerely doubt anything I could write would be as high quality. Still - I'm having fun tinkering and i'm curious about what others out there may think about certain methods i'm using.
Firstly, I'm using a homebrewed MySQLi 'DataLayer' Class to connect and perform basic CRUD functions. (Something of a Data Access Object, but not at all abstract)
Second, I have a login form that offers a visitor the oppurtunity to type in a username and password combination that gets sent via _post to the form validator/processor.
Please keep in mind that i'm still in the early stages of writing the login processing, it's not as clean as it could be. in the end, I intend on creating a subclass to handle the logins, just working out the logic first.
I first check if the user is logged in, in which case I regenerate the session id - I read this could help prevent session hi-jacking. Then I sanitize, doLogin is the variable associated with the submit button on the form. I don't know if this really & truly needs to be sanitized, but I don't think passing it through the filter hurts.
I'll post the code I have to process the form and i'll throw some add'l comments in where i'm looking for help.
<?php
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === TRUE) {
session_regenerate_id();
}
#
# Sanitizing form variables
#
$doLogin = filter_input(INPUT_POST, 'doLogin', FILTER_SANITIZE_STRING); /* var passed from the login form submit button */
$admin_login = filter_input(INPUT_POST, 'admin_login', FILTER_SANITIZE_STRING);
$admin_pass = filter_input(INPUT_POST, 'admin_pass', FILTER_SANITIZE_STRING);
$login_token = filter_input(INPUT_POST, 'login_token', FILTER_SANITIZE_STRING); /* copy of server generated token */
$err = "NULL"; # Still working on error handling - this is incomplete
# Ensuring we're processing strings
/* I read somewhere that ensuring you're dealing w/the exact type of data you expect is important. Someone, somewhere will find a way to throw other types at your processing methods. I'm almost certain that this is an unnecessary step at this point. Maybe if I put it before the filters, it may be useful? Not sure on this */
$var_chk = array($doLogin, $admin_login, $admin_pass);
foreach ($var_chk as $string) {
if (!is_string($string)) {
settype($string, 'string');
}
}
#
# Authenticating Server Token
#
if (isset($doLogin)) {
/* I'm certain some work needs to be done here- I get an error if TOKEN_EXP_TIME (in this case, it's defined as 180 seconds) has passed and the token expires.
Notice: Undefined index: login_token_time - at this time i'm not overly concerned with this so I haven't toyed with it much but I have some ideas to remedy this issue */
$login_token_age = time() - $_SESSION['login_token_time'];
if (!isset($login_token) || !isset($_SESSION['login_token']) || empty($login_token) || $login_token !== $_SESSION['login_token'] || $login_token_age > TOKEN_EXP_TIME) {
$err = "bad_token";
ImportantMsg::writeBadTokenMsg();
# disallow continuation
$continue = "0";
} else {
unset($_SESSION['login_token']);
unset($_SESSION['login_token_time']);
$continue = "1";
}
}
if ($continue === "1") {
#
# Validating the user
#
if (isset($doLogin) && $admin_login == '' || $admin_pass == '') {
$err = "inc_cred";
ImportantMsg::writeRequiredFieldMsg();
}
#
# If form was completed and submitted
#
if (isset($doLogin) && isset($admin_login) && isset($admin_pass) && $admin_login != '' && $admin_pass != '') {
#
# Verify this user exists in the database
#
$table = $dl->select_specific("id, user_access, user_login, user_first, user_last, user_email", "code_users", array('user_login' => $admin_login, 'user_pass' => Users::hash_pass($admin_pass)), "", '1') or $err = "inv_cred";
if ($err == "inv_cred") {
ImportantMsg::writeInvalidCredentialsMsg();
/* So I feel I need to explain the definition of the variable $table here; the select_specific function is part of my DataLayer class that allows to more easily write SQL statements on the fly.
I have another function that works alot like it called select() but it uses just a table name, sort & condition as arguments. Since i'm dealing with a password, I didn't want to select the password hash & other misc stuff so I chose not to use it here for obvious security reasons.
$table resolves to : SELECT id, blah, blah, etc, from code_users WHERE user_login = $admin_login AND user_pass = passwordhash LIMIT 1
To break it down a bit further the result set is returned as a multidimensional array
$table = array( # Typical breakdown of dataLayer select() method
array(
'id' => '10',
'user_access' => "admin",
'user_login' => 'j.doe',
'user_first' => 'Jonathan',
'user_last' => 'Doe',
'user_email' => 'j.doe@isp.com'
),
);
so $table[0]['id'] = 10 in this instance
I have a utility method designed that takes variables like $admin_login and passes them through mysqli_real_escape_string automatically - I'm working on learning PDO though so maybe one day soon i'll convert, but for now i love this datalayer!
*/
} else {
#
# If user exists - set sessions
#
# Define user_hash
$_SESSION['user_hash'] = md5(microtime());
# Insert user_hash on login
$dl->update("code_users", array('user_hash' => $_SESSION['user_hash']), array('id' => $table[0]['id'])) or die($dl->getError());
foreach ($table as $d_row) {
foreach ($d_row as $field => $val) {
$_SESSION[$field] = $val;
}
}
#
# Advance to home page
#
$_SESSION['loggedin'] = TRUE;
echo "<meta http-equiv=\"REFRESH\" content=\"3;url=$base_url/index.php\">";
ImportantMsg::writeLoginMsg();
}
}
}
So i'm really curious about the user_hash; It seems like a good idea to store such a thing in the database as I have a method called isLoggedIn() that checks for the session[user_hash] and matches it to the database variable and if they match, I know the user is currently logged in and I can display user/admin related material - of course, I only check the database if session[loggedin]==TRUE. Is it worthwhile to check for the user hash or is just checking the sessions good enough?
I think checking for the user_hash adds an extra layer of security and that's why i implemented it but if it's just a waste of resources, it's easily removed.
The last thing I wanted to discuss was how I'm hashing passwords. As shown above, I have a public static function in my Users class that does the work.
Firstly, I have two "Salts" if you will.
One is 63 random printable ASCII characters (Site Key)
Another is 63 random alpha-numeric characters (a-z, A-Z, 0-9) (Site Salt)
( https://www.grc.com/passwords.htm generated them for me )
Both of which are hard-coded. Right now, both are stored in a .php file with other config settings however, eventually, i will store the site key in the database eventually.
Then, I take the password, concatenate the site salt, user id then site key to it and spit out a hash; the function looks something like this:
function hash_pass($password) {
hash_hmac('sha512', $password . S_SALT . (int) self::getIDFromLogin(), S_KEY);
return $hashed_pw;
}
The only real difference between each hash will be the actual password and the id of course will be different for each user as the other two salts are static I'm curious about the integrity of this solution.
Would it be better to actually generate a random salt and store it in the database as opposed to using the users id? One would still have to connect to the database to fetch it so I doubt it would be less of a stressful operation on the server but given the length of the two static salts - a 1 or 2 digit user id is enough to make even the same password result in a different hash for each unique user
Should I use a different algo? I'm not looking for hashing speed - just strength really. I've tried the password_hash function but i'm using PHP 5.4.4-14+deb7u11 (cli) (built: Jun 16 2014 09:46:53) - the most up to date version of PHP available for Debian wheezy and it generates a fatal error to an undefined function so for now something like the function above will have to suffice.
I was considering creating a secondary hash_pass2() function that rearranged the salts and the id so that every user with odd (or even, whatever) id #'s would use the secondary hash but I haven't done this yet.
I'd sure love some feedback on this as any help is greatly appreciated to improve the code
For anyone curious - here's the getIDFromLogin function
public static function getIDFromLogin() {
$admin_login = filter_input(INPUT_POST, 'admin_login', FILTER_SANITIZE_STRING);
$count = $dl->select_specific('id, count(*) as count', 'code_users', array('user_login' => $admin_login), '', '1'); # SELECT id, count(*) as count FROM code_users WHERE user_login = $admin_login LIMIT 1
if ($count[0]['count'] > 0) {
return $count[0]['id'];
} else {
return false;
}
}