I wrote this class to handle login sessions for a framework I'm writing for educational purposes.
My major areas of concern:
Style
I know my style is a little contrary to most coding conventions. Is it difficult to read?
The nonce
If the login is valid, the session checker punts a random hash to a cookie to compare with the same hash stored in a session variable on the next request. Is this a proper nonce or just nonsense?
bcrypt
Plain and simple - Did I do it right?
PDO
I just started using PDO. Do my prepared statements look right?
Security
From a security standpoint, is this a disaster waiting to happen? Is it reasonably resistant to hijacking and injection? Am I handling passwords responsibly?
session.php:
define("SESS_NAME","PHPSESSNAME");
define("COOK_TIMEOUT",1500);
define("COOK_DOMAIN","localhost");
define("COOK_PATH","/");
define("COOK_SECURE",FALSE);
define("COOK_HTTP",TRUE);
class Session {
public $is_logged;
public $error;
private $useragent;
public function __construct(){
$this->useragent = $_SERVER["HTTP_USER_AGENT"];
session_name(SESS_NAME);
session_set_cookie_params(COOK_TIMEOUT,COOK_PATH,COOK_DOMAIN,COOK_SECURE,COOK_HTTP);
session_start();
}
public function set_session($user){
if(isset($_SESSION['user'])){
$this->error = "Session Already Set";
return false;
}
$nonce = $this->gen_nonce();
$_SESSION['user'] = $user;
$_SESSION['nonce'] = $nonce;
$_SESSION['useragent'] = $this->useragent;
setcookie('nuo',$nonce,time()+COOK_TIMEOUT);
header("refresh:0");
}
public function check_session(){
if(!isset($_SESSION['useragent']) ||
!isset($_SESSION['nonce'])
){
$this->error = "Session Verification Failed";
return false;
}
if(($this->useragent != $_SESSION['useragent']) ||
($_COOKIE['nuo'] != $_SESSION['nonce'])
){
$this->killsess();
$this->error = "Session Verification Failed";
return false;
}
$newnonce = $this->gen_nonce();
$_SESSION['nonce'] = $newnonce;
setcookie('nuo',$newnonce,time()+COOK_TIMEOUT);
$this->is_logged = true;
return true;
}
private function gen_nonce(){
$string = "";
for($i = 0; $i < 256; $i++){
$string .= chr(rand(0,255));
}
return md5($string);
}
private function killsess(){
foreach($_SESSION as $key =>$id ){
unset($_SESSION[$key]);
}
setcookie('nuo','',time() - 42000);
setcookie(SESS_NAME,'',time() - 42000);
session_destroy();
}
};
Authorization is handled by another class that uses PHP's password hashing functions.
bcrypt.php:
class Bcrypt {
public $error;
private $dbhand;
private $crypt_cost = 10;
public function __construct($dbhand){
if(!($dbhand instanceof PDO)) die('Database Object Invalid');
$this->dbhand = $dbhand;
if(CRYPT_BLOWFISH !== 1) die("Blowfish not supported");
}
public function create_user($user,$pass){
if($this->check_user_exists($user)){
$this->error = "User Already Exists";
return false;
}
if(empty($hash = $this->hash_pass($pass))){
$this->error = "Password Hash Failed";
return false;
}
$stmt = $this->dbhand->prepare("INSERT INTO users VALUES(NULL,?,?);");
if(!$stmt->execute(array(0=>$user,1=>$hash))){
$this->error = "Database Error: {$stmt->error}";
return false;
}
return true;
}
public function set_pass($user,$pass){
if(!$this->check_user_exists($user)){
$this->error = "User Does Not Exist";
return false;
}
if(!empty($hash = $this->hash_pass($pass))){
$stmt = $this->dbhand->prepare("UPDATE users SET pass = ? WHERE user = ?");
if($stmt->execute(array(0=>$hash,1=>$user))){
return true;
}
}
$this->error = "Set Password Failed";
return false;
}
public function change_pass($user,$oldpass,$newpass){
if($this->check_pass($user,$oldpass)){
if($this->set_pass($user,$newpass))
return true;
}
$this->error = "Password Change Failed";
return false;
}
public function check_pass($user,$pass){
if(!empty($hash = $this->get_hash($user))){
if(password_verify($pass,$hash)){
$this->check_hash($user,$pass,$hash);
return true;
}
}
$this->error = "Password Verification Failed";
return false;
}
private function check_user_exists($user){
$stmt = $this->dbhand->prepare("SELECT COUNT(user) FROM users WHERE user = ?");
$stmt->execute(array(0=>$user));
$result = $stmt->fetchAll();
if($result[0][0] > 0){
return true;
}
return false;
}
private function hash_pass($pass){
if(!empty($hash = password_hash($pass,PASSWORD_BCRYPT,array('cost'=>$this->crypt_cost)))){
return $hash;
}
return false;
}
private function check_hash($user,$pass,$hash){
if(password_needs_rehash($hash,PASSWORD_BCRYPT,array('cost'=>$this->crypt_cost))){
$this->set_pass($user,$pass);
}
}
private function get_hash($user){
$stmt = $this->dbhand->prepare("SELECT pass FROM users WHERE user = ?");
if(!$stmt->execute(array(0=>$user)))
return false;
$return = $stmt->fetchAll();
return $return[0]['pass'];
}
};
Implementation is simple:
require_once('bcrypt.php');
require_once('session.php');
$dbhand = new PDO('blahblah');
$bcryptobj = new Bcrypt($dbhand);
$sessobj = new Session;
if(!empty($_POST['userlogin'])){
if($bcryptobj->check_pass($_POST['username'],$_POST['password'])){
$sessobj->set_session($_POST['username']);
}
}
else {
$sessobj->check_session();
}