I know I'm missing something here. I've been staring at this short script for a while now and I can't see where it's going wrong.
Here's my script:
http://pastebin.com/FtNeNtwj
And here's the script in action:
http://troop007.tk/login.007?action=login
The username and password are both "demo". I type in the username and password, press log in, and instead of taking me to login.007?action=logincheck, it never leaves login.007?action=login.
I have one MySQL table called users, and inside that table there are two fields: username and password.
The script I'm using is a modified version of the script found here: http://www.phpeasystep.com/phptu/6.html
I would scrap that tutorial, login security is not tobe looked upon lightly, you should not have plaintext passwords in the database they should be hashed with a salt and both should change upon successful login.
Here is A secure login script:
It uses PDO for the database connection, the actual login form uses random keys for login eg. not username/password. Passwords are hashed with sha512 x 25k times and with a 16byte key salt, brute force protection. Hope it helps.
<?php
session_start();
/**
* Table
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(45) DEFAULT NULL,
`pass_hash` varchar(255) DEFAULT NULL,
`pass_salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;
*/
//DB Stuff
define('DBHOST','localhost');
define('DBNAME','yourdb');
define('DBUSER','root');
define('DBPASS','');
//End Config:---
//Open a PDO Database connection
try {
$db = new PDO("mysql:host=".DBHOST.";dbname=".DBNAME, DBUSER, DBPASS);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
}catch (Exception $e){
die('Cannot connect to mySQL server.');
}
class Login{
public $db;
public $user;
public $pass;
public $error;
// sha512
public $algo = '$6';
// Cost parameter, 25k iterations
public $cost = '$rounds=25000$';
function __construct(PDO $db){
$this->db = $db;
$this->global_salt = sha1($_SERVER['HTTP_HOST']);
}
/**
* Return a random seed for the mt_rand function
*/
function make_seed(){
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}
/**
* Return a random unique salt for new created hash/crypt function salts
*/
function unique_salt(){
$salt = null;
mt_srand($this->make_seed());
for($i=0;$i < mt_rand(1,10);$i++){
$salt = sha1($this->global_salt.$salt.mt_rand().uniqid().microtime(true));
}
return substr($salt,0,16);
}
/**
* Hash a given password and store parts in:
* $this->salt = a unique 16 byte salt
* $this->hash = The full crypted hash sting including algo/cost/salt/crytedpassword
* $this->full_salt = Just algo/cost/salt section, the first 33 bytes
* $this->hashed_password = Just crytedpassword section, proceeding bytes after first 33 bytes
*
*/
function hash($password){
$this->salt = $this->unique_salt();
$this->full_hash = crypt($password, $this->algo.$this->cost.$this->salt);
$this->full_salt = substr($this->full_hash, 0, 33);
$this->hashed_password = substr($this->full_hash, 33);
return $this->full_hash;
}
/**
* Method to validate the given crypto hash against the given password
*/
function check_password($hash, $salt, $password){
$hash = ($this->algo.$this->cost.$salt.'$'.$hash);
if($hash == crypt($password, substr($hash, 0, 33))){
//Regenerate new hash and salt for given password
$this->update_keys();
$this->status = true;
$_SESSION['logged_in']=true;
return true;
}else{
$this->status = false;
return false;
}
}
/**
* Set error
*/
function set_error($type,$value){
$this->error[$type]=$value;
}
/**
* Output error
*/
function error($type){
echo (isset($this->error[$type]))?$this->error[$type]:null;
}
/**
* Logout and regenirate session and redirect to index
*/
static function logout(){
unset($_SESSION['logged_in']);
session_regenerate_id(true);
exit(header('Location: ./index.php'));
}
function anti_brute($intval){
if(!isset($_SESSION['access_time'])){
$_SESSION['access_time']=time();
}else{
$t = time()-$_SESSION['access_time'];
if($t <= $intval){
$this->set_error('global','Time violation');
$_SESSION['access_time']=time();
return true;
}
$_SESSION['access_time']=time();
return false;
}
}
function process_login(){
if($_SERVER['REQUEST_METHOD']=='POST'){
$this->user = (isset($_SESSION['userParam']) && isset($_POST[$_SESSION['userParam']]))?$_POST[$_SESSION['userParam']]:null;
$this->pass = (isset($_SESSION['passParam']) && isset($_POST[$_SESSION['passParam']]))?$_POST[$_SESSION['passParam']]:null;
$this->create = (isset($_SESSION['createParam']) && isset($_POST[$_SESSION['createParam']]))?$_POST[$_SESSION['createParam']]:null;
$cont = true;
if($this->user == null || strlen($this->user) <= 2){$this->set_error('user','Please enter a username!'); $cont=false;}
if($this->pass == null || strlen($this->pass) <= 2){$this->set_error('pass','Please enter a password!'); $cont=false;}
if($cont==true){
//Alls good continue
if($this->create != null && $this->create=='1'){
//Check user for new account
if($this->check_user()==true){$this->set_error('user','Username already taken.');return;}
//Create account
$this->create_account();
}else{
//Stop really fast request 2 seconds
if($this->anti_brute(2)==false){
//Attempt to login
$this->check_login();
}
}
}else{
//Error with form
$this->set_error('global','Please fill in login form!');
}
}
}
function check_user(){
$sql = 'SELECT 1 FROM users WHERE username=:username';
$statement = $this->db->prepare($sql);
$statement->bindParam(':username', $this->user, PDO::PARAM_STR);
$statement->execute();
$result = $statement->fetch(PDO::FETCH_ASSOC);
if(!empty($result)){return true;}else{return false;}
}
function check_login(){
$sql = 'SELECT pass_hash, pass_salt FROM users WHERE username=:username';
$statement = $this->db->prepare($sql);
$statement->bindParam(':username', $this->user, PDO::PARAM_STR);
$statement->execute();
$result = $statement->fetch(PDO::FETCH_ASSOC);
$this->check_password($result['pass_hash'], $result['pass_salt'], $this->pass);
}
function create_account(){
//Create new account
$this->hash($this->pass);
$sql = 'INSERT into users (username, pass_hash, pass_salt) VALUES (:username, :pass_hash, :pass_salt)';
$statement = $this->db->prepare($sql);
$statement->bindParam(':username', $this->user, PDO::PARAM_STR);
$statement->bindParam(':pass_hash', $this->hashed_password, PDO::PARAM_STR);
$statement->bindParam(':pass_salt', $this->salt, PDO::PARAM_STR);
$statement->execute();
$this->status = true;
$_SESSION['logged_in']=true;
}
function update_keys(){
//Update account password hash & salt
$this->hash($this->pass);
$sql = 'UPDATE users SET pass_hash=:pass_hash, pass_salt=:pass_salt WHERE username=:username';
$statement = $this->db->prepare($sql);
$statement->bindParam(':username', $this->user, PDO::PARAM_STR);
$statement->bindParam(':pass_hash', $this->hashed_password, PDO::PARAM_STR);
$statement->bindParam(':pass_salt', $this->salt, PDO::PARAM_STR);
$statement->execute();
$this->status = true;
$_SESSION['logged_in']=true;
}
}//END Login class
//Logout handler
if(isset($_GET['logout'])){ Login::logout(); }
$login = new Login($db);
//Login handler
$login->process_login();
//Debug
echo '<pre>';
print_r($login);
echo '</pre>';
//Check login status
if(isset($_SESSION['logged_in']) && $_SESSION['logged_in']==true){
//Logged in
echo 'Logout';
}else{
//Not Logged In
//Show login form & create uniqie parrams for user/pass/create post keys
$_SESSION['userParam'] = sha1(uniqid().microtime(true));
$_SESSION['passParam'] = sha1(uniqid().microtime(true));
$_SESSION['createParam'] = sha1(uniqid().microtime(true));
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Secure Login</title>
</head>
<body>
<h1>Secure Login Example</h1>
<h3>Please login:</h3>
<?php $login->error('global'); ?>
<form method="POST" action="">
<label for="user">Username : </label>
<input type="text" name="<?=$_SESSION['userParam'];?>" size="29"> <?php $login->error('user'); ?>
<br />
<label for="pass">Password : </label>
<input type="text" name="<?=$_SESSION['passParam'];?>" size="29"> <?php $login->error('pass'); ?>
<br />
<input type="submit" value="Login"> and create my account:<input type="checkbox" name="<?=$_SESSION['createParam'];?>" value="1">
</form>
</body>
</html>
<?php } ?>
Related
I have three files that are relevant for this part of my login scenario:
/project/index.html
/project/api/user/login.php
/project/api/objects/user.php
The index.html has a simple login form in it, calling the ./api/user/login.php.
In this form I have a checkbox that is an option for the user in order to stay logged in or not.
If the user has selected this option, with every login, I would like to check if the credentials are correct (login function -> stmt1 in user.php) as well as to update the lastlogin (datetime), the identifier and securitytoken if the checkbox was set (login function -> stmt2 in user.php).
The user.php is included_once in the login.php that gets the values out of the index.html form and sends them to the login() function in the user.php.
Depending on the functions return value, the login.php decides if the login was successful or not.
The login itself (stmt1) works, but the update of lastlogin, identifier and securitytoken (stmt2) doesn't.
login.php
session_start();
// include database and object files
include_once '../config/database.php';
include_once '../objects/user.php';
// get database connection
$database = new Database();
$db = $database->getConnection();
// prepare user object
$user = new User($db);
// set ID property of user to be edited
$user->username = isset($_GET['username']) ? $_GET['username'] : die();
$user->password = base64_encode(isset($_GET['password']) ? $_GET['password'] : die());
$user->remember = isset($_GET['remember']) ? $_GET['remember'] : die();
$stmt1 = $user->login();
if($stmt1->rowCount() > 0){
// get retrieved row
$row1 = $stmt1->fetch(PDO::FETCH_ASSOC);
$_SESSION['userid'] = $row1['uid'];
// create array
$user_arr=array(
"status" => true,
"message" => "Login erfolgreich!",
"uid" => $row1['uid'],
"username" => $row1['username']
);
$stmt2 = $user->login();
$row2 = $stmt2->fetch(PDO::FETCH_ASSOC);
print_r($row2);
// create array
$user_arr=array(
"lastlogin" => $row2['lastlogin']
);
}
else{
$user_arr=array(
"status" => false,
"message" => "Benutzername und/oder Passwort nicht korrekt!",
);
}
// make it json format
print_r(json_encode($user_arr));
?>
user.php
function login(){
// select all query
$query1 = "SELECT
`uid`, `username`, `email`, `password`, `created`, `lastlogin`
FROM
" . $this->table_name . "
WHERE
username='".$this->username."' AND password='".$this->password."'";
// prepare query statement
$stmt1 = $this->conn->prepare($query1);
// execute query
$stmt1->execute();
return $stmt1;
// set up the remain logged in function
if(isset($this->remember)) {
$identifier = random_string();
$securitytoken = random_string();
$remember = ",identifier='".$identifier."',securitytoken='".$securitytoken."'";
setcookie("identifier",$identifier,time()+(3600*24*365)); //1 year valid
setcookie("securitytoken",$securitytoken,time()+(3600*24*365)); //1 year valid
} else {
$remember = "";
}
// update last login
$query2 = "UPDATE
" . $this->table_name . "
SET
`lastlogin` = '".date("Y-m-d H:i:s")."'
".$remember."
WHERE
username='".$this->username."' AND password='".$this->password."'";
// prepare query statement
$stmt2 = $this->conn->prepare($query2);
// execute query
$stmt2->execute();
return $stmt2;
}
function random_string(){
if(function_exists('random_bytes')) {
$bytes = random_bytes(16);
$str = bin2hex($bytes);
} else if(function_exists('openssl_random_pseudo_bytes')) {
$bytes = openssl_random_pseudo_bytes(16);
$str = bin2hex($bytes);
} else if(function_exists('mcrypt_create_iv')) {
$bytes = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$str = bin2hex($bytes);
} else {
//secret key should have >12 random chars
$str = md5(uniqid('SECRET KEY', true));
}
return $str;
}
In the user.php after return $stmt1;
The code is returned and the cookies are not set
I would do this... Check login... If true, save cookies with id and token
And then periodically check if token and id correspond... If so... Just UPDATE the last login time.
Note: your prepared statement is vulnerable!! Dont append the parameters with '.' use placeholders instead, and dont encode the password, is better to hash it... Then compare hashes
I have been working on a website on a localhost and have just tried to upload it to a free webserver so I can get some testers, for some reason my code is being reported as malware and is being blocked by my antivirus, this means I can't see anything when visiting it apart from the ERR_CONNECTION_RESET. Have you guys got any ideas as to why this code is being detected as malware?
LOGIN.php
<?php
include('classes/db.php');
if (db::maintenance()) {
die('This site is currently going under maintenance, please check back again shortly.');
}
if (isset($_POST['submit'])) {
$username = $_POST['username'];
$password = $_POST['password'];
if (db::query('SELECT username FROM users WHERE username=:username', array(':username'=>$username))) {
if (password_verify($password, db::query('SELECT password FROM users WHERE username=:username', array(':username'=>$username))[0]['password'])) {
echo "Logged in!";
$cstrong = True;
$token = bin2hex(openssl_random_pseudo_bytes(64, $cstrong));
$user_id = db::query('SELECT id FROM users WHERE username=:username', array(':username'=>$username))[0]['id'];
db::query('INSERT INTO login_tokens VALUES (NULL, :token, :user_id)', array(':token'=>sha1($token), ':user_id'=>$user_id));
setcookie("SNID", $token, time() + 60 * 60 * 24 * 7, '/', NULL, NULL, TRUE);
setcookie('SNID_', '1', time() + 60 + 60 * 24 * 3, '/', NULL, NULL, TRUE);
header('Location: index.php');
} else {
echo "Incorrect password";
}
} else {
echo "User not registered!";
}
}
?>
<h1>Login to your account</h1>
<form action="login.php" method="post">
<input type="text" name="username" value="" placeholder="Username"><p />
<input type="password" name="password" value="" placeholder="Password"><p />
<input type="submit" name="submit" placeholder="Login"><p />
</form>
DB.php
(I have changed the connection to false data, and changed it to the correct data when uploading it to the host.)
<?php
class db {
private static function connect () {
$conn = new PDO('mysql:host=localhost;dbname=users;,charset=utf8', 'root', '');
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $conn;
}
public static function query ($sql, $params = array()) {
$statement = self::connect()->prepare($sql);
$statement->execute($params);
if (explode(' ', $sql)[0] == 'SELECT') {
$result = $statement->fetchAll();
return $result;
}
}
public static function notify ($userid) {
$notifications = db::query('SELECT forum_members.forum_id, notifications.user_id, notifications.post_id, notifications.forum_id, notifications.post_body, notifications.creation, notifications.type FROM forum_members, notifications WHERE (notifications.forum_id=forum_members.forum_id OR notifications.forum_id=0) AND notifications.user_id=forum_members.user_id ORDER BY notifications.post_id DESC');
return $notifications;
}
public static function maintenance () {
return false;
}
}
?>
Which type of address do you use to enter the website? PHP source doesn't display to browsers, so PHP isn't the problem.
If you enter in with a hostname (Ex. .....2cc.brad....net) Then it'll automatically get detected as a "malware" for beginner safety, if ur accessing it from localhost/127.0.0.1 it should be fine, but if ur accessing it from a host that's marked as malware, than yep.
Updates:
9th Apr:
I have checked where the problem is.
First of all i added an echo in process_login.php and I deleted the header('location...protected_page.php). The login function (in functions.php) does his job, and writes CORRECTLY: $_SESSION['user_id'] as well as $_SESSION['username'] and $_SESSION['login_string'].
Then I deleted the echos in process_login.php and rewrote the header('location.. to protected_page.php.(i back to the original code). And after that added some echos in login_check before any if function login_check($mysqli) {
echo "<br>userid:".$_SESSION['user_id'];
echo "<br>username:".$_SESSION['username'];
echo "<br>loginstring:".$_SESSION['login_string'];
The result is blank, so the SESSION variables are wroten correctly in function login but after the header to protected_page the variables seem to be deleted. Maybe sec_session_start() function is not working well? Anyway i will follow #Manikiran 's advices and I will continue working, removing encryptions or something.... Thank you #Manikiran and #lps
Original questions:
I am trying to do a basic login system. However, what i've done doesn't work. The system is basically an index.php where you put your credentials, then the php verifies if your user exists and if the password is correct. Then the user is redirected to protected_page.php. Okay, the authentication part works fine, when the user and password is correct the web page redirects you to protected_page.php and when the user and password is not correct, the page tells you so.
Now, the problem: in protected_page.php there is obviously a session "checker", i mean if you connected correctly with your user and password, the page will show you private html information, if you're not connected the page shouldn't show you anything. This is what doesn't works, despite you connect correctly with a user, the page tells you you're not logged in....
(note: the index.php also contains protected html which doesn't work neither)
The code:
(note:sec_session_starts() is suposed to be a way to prevent session hijacking and it contains of course session_start())
index.php: (i guess it works fine, as it redirects to protected_page() when it has to do it. Maybe the problem is the login(...) function in the part where I set the session data...).
<?php
include_once 'includes/db_connect.php';
include_once 'includes/functions.php';
sec_session_start();
if (login_check($mysqli) == true) {
$logged = 'in';
} else {
$logged = 'out';
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Secure Login: Log In</title>
<script type="text/JavaScript" src="js/sha512.js"></script>
<script type="text/JavaScript" src="js/forms.js"></script>
</head>
<body>
<?php
if (isset($_GET['error'])) {
echo '<p class="error">Error Logging In!</p>';
}
?>
<form action="includes/process_login.php" method="post" name="login_form">
Email: <input type="text" name="email" />
Password: <input type="password"
name="password"
id="password"/>
<input type="button"
value="Login"
onclick="formhash(this.form, this.form.password);" />
</form>
<?php
if (login_check($mysqli) == true) {
echo '<p>Currently logged ' . $logged . ' as ' . htmlentities($_SESSION['username']) . '.</p>';
echo '<p>Do you want to change user? Log out.</p>';
} else {
echo '<p>Currently logged ' . $logged . '.</p>';
echo "<p>If you don't have a login, please <a href='register.php'>register</a></p>";
}
?>
</body>
</html>
process_login.php (works fine i guess)
<?php
include_once 'db_connect.php';
include_once 'functions.php';
sec_session_start(); // Our custom secure way of starting a PHP session.
if (isset($_POST['email'], $_POST['p'])) {
$email = $_POST['email'];
$password = $_POST['p']; // The hashed password.
if (login($email, $password, $mysqli) == true) {
// Login success
header('Location: ../protected_page.php');
} else {
// Login failed
header('Location: ../index.php?error=1');
}
} else {
// The correct POST variables were not sent to this page.
echo 'Invalid Request';
}
functions.php:
<?php
include_once 'psl-config.php';
require_once 'passwordLib.php';
function sec_session_start() {
$session_name = 'sec_session_id'; // Set a custom session name
$secure = true;
// This stops JavaScript being able to access the session id.
$httponly = true;
// Forces sessions to only use cookies.
if (ini_set('session.use_only_cookies', 1) === FALSE) {
header("Location: ../error.php?err=Could not initiate a safe session (ini_set)");
exit();
}
// Gets current cookies params.
$cookieParams = session_get_cookie_params();
session_set_cookie_params($cookieParams["lifetime"],
$cookieParams["path"],
$cookieParams["domain"],
$secure,
$httponly);
// Sets the session name to the one set above.
session_name($session_name);
session_start(); // Start the PHP session
session_regenerate_id(true); // regenerated the session, delete the old one.
}
function login($email, $password, $mysqli) {
// Using prepared statements means that SQL injection is not possible.
if ($stmt = $mysqli->prepare("SELECT id, username, password
FROM members
WHERE email = ?
LIMIT 1")) {
$stmt->bind_param('s', $email); // Bind "$email" to parameter.
$stmt->execute(); // Execute the prepared query.
$stmt->store_result();
// get variables from result.
$stmt->bind_result($user_id, $username, $db_password);
$stmt->fetch();
if ($stmt->num_rows == 1) {
// If the user exists we check if the account is locked
// from too many login attempts
if (checkbrute($user_id, $mysqli) == true) {
// Account is locked
// Send an email to user saying their account is locked
return false;
} else {
// Check if the password in the database matches
// the password the user submitted. We are using
// the password_verify function to avoid timing attacks.
if (password_verify($password, $db_password)) {
// Password is correct!
// Get the user-agent string of the user.
$user_browser = $_SERVER['HTTP_USER_AGENT'];
// XSS protection as we might print this value
$user_id = preg_replace("/[^0-9]+/", "", $user_id);
$_SESSION['user_id'] = $user_id;
// XSS protection as we might print this value
$username = preg_replace("/[^a-zA-Z0-9_\-]+/",
"",
$username);
$_SESSION['username'] = $username;
$_SESSION['login_string'] = hash('sha512',
$db_password . $user_browser);
// Login successful.
return true;
} else {
// Password is not correct
// We record this attempt in the database
$now = time();
$mysqli->query("INSERT INTO login_attempts(user_id, time)
VALUES ('$user_id', '$now')");
return false;
}
}
} else {
// No user exists.
return false;
}
}
}
function checkbrute($user_id, $mysqli) {
// Get timestamp of current time
$now = time();
// All login attempts are counted from the past 2 hours.
$valid_attempts = $now - (2 * 60 * 60);
if ($stmt = $mysqli->prepare("SELECT time
FROM login_attempts
WHERE user_id = ?
AND time > '$valid_attempts'")) {
$stmt->bind_param('i', $user_id);
// Execute the prepared query.
$stmt->execute();
$stmt->store_result();
// If there have been more than 5 failed logins
if ($stmt->num_rows > 5) {
return true;
} else {
return false;
}
}
}
function login_check($mysqli) {
// Check if all session variables are set
if (isset($_SESSION['user_id'],
$_SESSION['username'],
$_SESSION['login_string'])) {
$user_id = $_SESSION['user_id'];
$login_string = $_SESSION['login_string'];
$username = $_SESSION['username'];
// Get the user-agent string of the user.
$user_browser = $_SERVER['HTTP_USER_AGENT'];
if ($stmt = $mysqli->prepare("SELECT password
FROM members
WHERE id = ? LIMIT 1")) {
// Bind "$user_id" to parameter.
$stmt->bind_param('i', $user_id);
$stmt->execute(); // Execute the prepared query.
$stmt->store_result();
if ($stmt->num_rows == 1) {
// If the user exists get variables from result.
$stmt->bind_result($password);
$stmt->fetch();
$login_check = hash('sha512', $password . $user_browser);
if (hash_equals($login_check, $login_string) ){
// Logged In!!!!
return true;
} else {
// Not logged in
return false;
}
} else {
// Not logged in
return false;
}
} else {
// Not logged in
return false;
}
} else {
// Not logged in
return false;
}
}
function esc_url($url) {
if ('' == $url) {
return $url;
}
$url = preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%#$\|*\'()\\x80-\\xff]|i', '', $url);
$strip = array('%0d', '%0a', '%0D', '%0A');
$url = (string) $url;
$count = 1;
while ($count) {
$url = str_replace($strip, '', $url, $count);
}
$url = str_replace(';//', '://', $url);
$url = htmlentities($url);
$url = str_replace('&', '&', $url);
$url = str_replace("'", ''', $url);
if ($url[0] !== '/') {
// We're only interested in relative links from $_SERVER['PHP_SELF']
return '';
} else {
return $url;
}
}
protected_page.php: (or maybe the problem is at login_check where it chekcs the session data
<?php
include_once 'includes/db_connect.php';
include_once 'includes/functions.php';
sec_session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Secure Login: Protected Page</title>
<link rel="stylesheet" href="styles/main.css" />
</head>
<body>
<?php if (login_check($mysqli) == true) : ?>
<p>Welcome <?php echo htmlentities($_SESSION['username']); ?>!</p>
<p>
This is an example protected page. To access this page, users
must be logged in. At some stage, we'll also check the role of
the user, so pages will be able to determine the type of user
authorised to access the page.
</p>
<p>Return to login page</p>
<?php else : ?>
<p>
<span class="error">You are not authorized to access this page.</span> Please login.
</p>
<?php endif; ?>
</body>
</html>
psl-config and db_connect connects with the db and it's working fine i think. passwordLib.php is a library as my server has php 5.4 and i am using php 5 functions. This library is suposed to help with some passwords encryptions or whatever and i think the problem is not here neither. Anyway I put the code:
<?php
/**
* PHP 5.5-like password hashing functions
*
* Provides a password_hash() and password_verify() function as appeared in PHP 5.5.0
*
* See: http://php.net/password_hash and http://php.net/password_verify
*
* #link https://github.com/Antnee/phpPasswordHashingLib
*/
require_once('passwordLibClass.php');
if (!function_exists('password_hash')){
function password_hash($password, $algo=PASSWORD_DEFAULT, $options=array()){
$crypt = NEW Antnee\PhpPasswordLib\PhpPasswordLib;
$crypt->setAlgorithm($algo);
$debug = isset($options['debug'])
? $options['debug']
: NULL;
$password = $crypt->generateCryptPassword($password, $options, $debug);
return $password;
}
}
if (!function_exists('password_verify')){
function password_verify($password, $hash){
return (crypt($password, $hash) === $hash);
}
}
if (!function_exists('password_needs_rehash')){
function password_needs_rehash($hash, $algo, $options=array()){
$crypt = NEW Antnee\PhpPasswordLib\PhpPasswordLib;
return !$crypt->verifyCryptSetting($hash, $algo, $options);
}
}
if (!function_exists('password_get_info')){
function password_get_info($hash){
$crypt = NEW Antnee\PhpPasswordLib\PhpPasswordLib;
return $crypt->getInfo($hash);
}
}
passwordlibclass.php:
<?php
/**
* PHP 5.5-like password hashing functions
*
* Provides a password_hash() and password_verify() function as appeared in PHP 5.5.0
*
* See: http://php.net/password_hash and http://php.net/password_verify
*
* #link https://github.com/Antnee/phpPasswordHashingLib
*/
namespace Antnee\PhpPasswordLib;
if (!defined('PASSWORD_BCRYPT')) define('PASSWORD_BCRYPT', 1);
// Note that SHA hashes are not implemented in password_hash() or password_verify() in PHP 5.5
// and are not recommended for use. Recommend only the default BCrypt option
if (!defined('PASSWORD_SHA256')) define('PASSWORD_SHA256', -1);
if (!defined('PASSWORD_SHA512')) define('PASSWORD_SHA512', -2);
if (!defined('PASSWORD_DEFAULT')) define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
class PhpPasswordLib{
CONST BLOWFISH_CHAR_RANGE = './0123456789ABCDEFGHIJKLMONPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
CONST BLOWFISH_CRYPT_SETTING = '$2a$';
CONST BLOWFISH_CRYPT_SETTING_ALT = '$2y$'; // Available from PHP 5.3.7
CONST BLOWFISH_ROUNDS = 10;
CONST BLOWFISH_NAME = 'bcrypt';
// Note that SHA hashes are not implemented in password_hash() or password_verify() in PHP 5.5
// and are not recommended for use. Recommend only the default BCrypt option
CONST SHA256_CHAR_RANGE = './0123456789ABCDEFGHIJKLMONPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
CONST SHA256_CRYPT_SETTING = '$5$';
CONST SHA256_ROUNDS = 5000;
CONST SHA256_NAME = 'sha256';
CONST SHA512_CHAR_RANGE = './0123456789ABCDEFGHIJKLMONPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
CONST SHA512_CRYPT_SETTING = '$6$';
CONST SHA512_ROUNDS = 5000;
CONST SHA512_NAME = 'sha512';
/**
* Default Crypt Algorithm
*
* #var INT
*/
private $algorithm = PASSWORD_BCRYPT;
/**
* Name of the current algorithm
*
* #var STRING
*/
private $algoName;
/**
* Setting for PHP Crypt function, defines algorithm
*
* Default setting is '$2a$' : BCrypt
*
* #var STRING
*/
protected $cryptSetting;
/**
* Setting for PHP Crypt function, defines processing cost
*
* Default setting is '08$' for BCrypt rounds
*
* #var INT
*/
protected $rounds;
/**
* Salt Character Count for Crypt Functions
*
* #var INT
*/
protected $addSaltChars;
/**
* Salt Character Range for Crypt Functions
*
* #var STRING
*/
protected $saltCharRange;
/**
* Class Constructor
*/
public function __construct(){
// Initialise default algorithm
$this->setAlgorithm($this->algorithm);
}
/**
* Generate Crypt Password
*
* #param STRING $password The password to encode
* #param ARRAY $options Cost value, and Salt if required
* #param BOOL $debug If true will return time to calculate hash
* #return STRING The encoded password
*/
public function generateCryptPassword($password, $options = array(), $debug = FALSE){
$startTime = microtime(TRUE);
if (isset($options['cost'])) $this->setCost($options['cost']);
$salt = $this->cryptSalt(#$options['salt']);
$crypt = crypt($password, $salt);
$endTime = microtime(TRUE);
if ($debug){
$calcTime = $endTime - $startTime;
return $calcTime;
}
return $crypt;
}
/**
* Generate Crypt Salt
*
* Generates a salt suitable for Crypt using the defined crypt settings
*
* #param STRING $salt Override random salt with predefined value
* #return STRING
*/
public function cryptSalt($salt=NULL){
if (empty($salt)){
for ($i = 0; $i<$this->addSaltChars; $i++){
$salt .= $this->saltCharRange[rand(0,(strlen($this->saltCharRange)-1))];
}
}
$salt = $this->cryptSetting.$this->rounds.$salt.'$';
return $salt;
}
/**
* Set Crypt Setting
*
* #param type $setting
* #return \Antnee\PhpPasswordLib\PhpPasswordLib
*/
public function cryptSetting($setting){
$this->cryptSetting = $setting;
return $this;
}
/**
* Salt Character Count
*
* #param INT $count Number of characters to set
* #return \Antnee\PhpPasswordLib\PhpPasswordLib|boolean
*/
public function addSaltChars($count){
if (is_int($count)){
$this->addSaltChars = $count;
return $this;
} else {
return FALSE;
}
}
/**
* Salt Character Range
*
* #param STRING $chars
* #return \Antnee\PhpPasswordLib\PhpPasswordLib|boolean
*/
public function saltCharRange($chars){
if (is_string($chars)){
$this->saltCharRange = $chars;
return $this;
} else {
return FALSE;
}
}
/**
* Set Crypt Algorithm
*
* #param INT $algo
* #return \Antnee\PhpPasswordLib\PhpPasswordLib
*/
public function setAlgorithm($algo=NULL){
switch ($algo){
case PASSWORD_SHA256:
$this->algorithm = PASSWORD_SHA256;
$this->cryptSetting(self::SHA256_CRYPT_SETTING);
$this->setCost(self::SHA256_ROUNDS);
$this->addSaltChars(16);
$this->saltCharRange(self::SHA256_CHAR_RANGE);
$this->algoName = self::SHA256_NAME;
break;
case PASSWORD_SHA512:
$this->algorithm = PASSWORD_SHA512;
$this->cryptSetting(self::SHA512_CRYPT_SETTING);
$this->setCost(self::SHA512_ROUNDS);
$this->addSaltChars(16);
$this->saltCharRange(self::SHA512_CHAR_RANGE);
$this->algoName = self::SHA512_NAME;
break;
case PASSWORD_BCRYPT:
default:
$this->algorithm = PASSWORD_BCRYPT;
if (version_compare(PHP_VERSION, '5.3.7') >= 1){
// Use improved Blowfish algorithm if supported
$this->cryptSetting(self::BLOWFISH_CRYPT_SETTING_ALT);
} else {
$this->cryptSetting(self::BLOWFISH_CRYPT_SETTING);
}
$this->setCost(self::BLOWFISH_ROUNDS);
$this->addSaltChars(22);
$this->saltCharRange(self::BLOWFISH_CHAR_RANGE);
$this->algoName = self::BLOWFISH_NAME;
break;
}
return $this;
}
/**
* Set Cost
*
* #todo implement
*
* #return \Antnee\PhpPasswordLib\PhpPasswordLib
*/
public function setCost($rounds){
switch ($this->algorithm){
case PASSWORD_BCRYPT:
$this->rounds = $this->setBlowfishCost($rounds);
break;
case PASSWORD_SHA256:
case PASSWORD_SHA512:
$this->rounds = $this->setShaCost($rounds);
break;
}
return $this;
}
/**
* Set Blowfish hash cost
*
* Minimum 4, maximum 31. Value is base-2 log of actual number of rounds, so
* 4 = 16, 8 = 256, 16 = 65,536 and 31 = 2,147,483,648
* Defaults to 8 if value is out of range or incorrect type
*
* #param int $rounds
* #return STRING
*/
private function setBlowfishCost($rounds){
if (!is_int($rounds) || $rounds < 4 || $rounds > 31){
$rounds = $rounds = self::BLOWFISH_ROUNDS;
}
return sprintf("%02d", $rounds)."$";
}
/**
* Set SHA hash cost
*
* Minimum 1000, maximum 999,999,999
* Defaults to 5000 if value is out of range or incorrect type
*
* #param INT $rounds
* #return STRING
*/
private function setShaCost($rounds){
if (!is_int($rounds) || $rounds < 1000 || $rounds > 999999999){
switch ($this->algorithm){
case PASSWORD_SHA256:
$rounds = self::SHA256_ROUNDS;
case PASSWORD_SHA512:
default:
$rounds = self::SHA512_ROUNDS;
}
}
return "rounds=" . $rounds ."$";
}
/**
* Get hash info
*
* #param STRING $hash
* #return ARRAY
*/
public function getInfo($hash){
$params = explode("$", $hash);
if (count($params) < 4) return FALSE;
switch ($params['1']){
case '2a':
case '2y':
case '2x':
$algo = PASSWORD_BCRYPT;
$algoName = self::BLOWFISH_NAME;
break;
case '5':
$algo = PASSWORD_SHA256;
$algoName = self::SHA256_NAME;
break;
case '6':
$algo = PASSWORD_SHA512;
$algoName = self::SHA512_NAME;
break;
default:
return FALSE;
}
$cost = preg_replace("/[^0-9,.]/", "", $params['2']);
return array(
'algo' => $algo,
'algoName' => $algoName,
'options' => array(
'cost' => $cost
),
);
}
/**
* Verify Crypt Setting
*
* Checks that the hash provided is encrypted at the current settings or not,
* returning BOOL accordingly
*
* #param STRING $hash
* #return BOOL
*/
public function verifyCryptSetting($hash, $algo, $options=array()){
$this->setAlgorithm($algo);
if (isset($options['cost'])) $this->setCost($options['cost']);
$setting = $this->cryptSetting.$this->rounds;
return (substr($hash, 0, strlen($setting)) === $setting);
}
}
Final comment: I promise i have searched a lot but not found this particular problem in other questions. Thank you.
The problem is that hash_equals() is not working well, despite the two variables compared are exactly the same, that function returns false. I have tried to change it to different libraries, but no way. (I remember my host has php 5.4 and i cannot use password_hash() function.
The only solution is to change that or find a library that replaces password_hash() correctly.
When I try to call the function "getAverage" within "storeRating", I get a HTML 500 server error. If I comment out that function, everything works perfectly. How can I call function "getAverage" withing function "storeRating"? Even if I leave that function uncommented, the code still checks for a duplicate rating and posts the new rating to the "rating" table. Please look at my code at the getAverage function. I need to be able to update the rating in the "products" table with the average.
Here are my PHP classes.
DB Functions:
<?php
class DB_TestFunctions {
private $conn;
// constructor
function __construct() {
require_once 'DB_Connect.php';
// connecting to database
$db = new Db_Connect();
$this->conn = $db->connect();
}
// destructor
function __destruct() {
}
// Storing new rating
public function storeRating($pid, $userid, $ratingpnt) {
$stmt = $this->conn->prepare("INSERT INTO rating(ProductID,UserID,prod_rating) VALUES(?, ?, ?)");
$stmt->bind_param("sss", $pid, $userid, $ratingpnt);
$result = $stmt->execute();
$stmt->close();
getAverage($pid);
// check for successful store
/* if ($result) {
$stmt = $this->conn->prepare("SELECT * FROM products WHERE pid = ?");
$stmt->bind_param("s", $pid);
$stmt->execute();
$rating = $stmt->get_result()->fetch_assoc();
$stmt->close();
return $rating;
} else {
return false;
} */
}
/**
* Check if rating exists
*/
public function checkDuplicate($pid, $userid) {
$stmt = $this->conn->prepare("SELECT prod_rating from rating WHERE ProductID = ? AND UserID = ?");
$stmt->bind_param("ss", $pid, $userid);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
// user existed
$stmt->close();
return true;
} else {
// user not existed
$stmt->close();
return false;
}
}
public function getAverage($pid){
$stmt = $this->conn->prepare("UPDATE products SET prod_rating = (SELECT AVG(prod_rating) FROM rating WHERE ProductID = ?) WHERE pid = ?");
$stmt->bind_param("s", $pid);
$stmt->execute();
$stmt->close();
}
public function getNewRating($pid){
$stmt = $this->conn->prepare("SELECT * FROM products WHERE pid = ?");
$stmt->bind_param("s", $pid);
$stmt->execute();
$rating = $stmt->get_result()->fetch_assoc();
$stmt->close();
return $rating;
}
}
?>
postRate
<?php
require_once 'include/DB_TestFunctions.php';
$db = new DB_TestFunctions();
// json response array
$response = array("error" => FALSE);
if (isset($_POST['pid']) && isset($_POST['userid']) && isset($_POST['rating'])) {
// receiving the post params
$pid = $_POST['pid'];
$userid = $_POST['userid'];
$rating = $_POST['rating'];
// check if user already rated product
if ($db->checkDuplicate($pid, $userid)) {
// user already rated this product
$response["error"] = TRUE;
$response["error_msg"] = "Rating already exists." ;
echo json_encode($response);
} else {
$db->storeRating($pid, $userid, $rating);
// get new rating
$rating = $db->getNewRating($pid);
if ($rating) {
// Rating successful
$response["error"] = FALSE;
$response["prod_rating"] = $rating["prod_rating"];
echo json_encode($response);
} else {
// Rating failed
$response["error"] = TRUE;
$response["error_msg"] = "Unknown error occurred in posting rating!";
echo json_encode($response);
}
}
} else {
$response["error"] = TRUE;
$response["error_msg"] = "Required parameters (pid, userid or rating) are missing!";
echo json_encode($response);
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Test Post Rating</title>
</head>
<body>
<h1>Add Comment</h1>
<form action="postRate.php" method="post">
Product ID:<br />
<input type="text" name="pid" placeholder="Product ID" />
<br /><br />
Userid:<br />
<input type="text" name="userid" placeholder="Username" />
<br /><br />
Rating:<br />
<input type="text" name="rating" placeholder="rating" />
<br /><br />
<input type="submit" value="Rate" />
</form>
</body>
</html>
The problem is that you are calling getAverage() that is a method of yor class.
So you need to $this, that is a reference to the current object, in order to call that function from your object.
Changing your code to :
$this->getAverage()
will solve your problem.
You need to call $this->getAverage()
Probably you should have a look at the PHP manual
I have a php/mysql poll that I'm trying to modify.
It sets a unique cookie if you've voted and blocks you from voting twice on the same poll if the cookie is set. If it set then you can only see the results.
What I would like to do is block the user from voting more than once if their IP is in the database as well (I know sessions would be better but not possible for my situation).
I've managed to grab the IP and store it in the db but I can't figure out the logic to tell the poll if it's in the database or not.
Can someone point me in the right direction?
The cookie detection is in the constructor and vote methods. The cookie is set at the end of the vote method.
<?php
# don't display errors
ini_set('display_errors',0);
error_reporting(E_ALL|E_STRICT);
class webPoll {
# makes some things more readable later
const POLL = true;
const VOTES = false;
# number of pixels for 1% on display bars
public $scale = 2;
public $question = '';
public $answers = array();
private $header = '<form class="webPoll" method="post" action="%src%">
<input type="hidden" name="QID" value="%qid%"/>
<h4>%question%</h4>
<fieldset><ul>';
private $center = '';
private $footer = "\n</ul></fieldset>%button%\n%totalvotes%\n</form>\n";
private $button = '<p class="buttons"><button type="submit" class="vote">Vote!</button></p>';
private $totalvotes = '';
private $md5 = '';
private $id = '';
/**
* ---
* Takes an array containing the question and list of answers as an
* argument. Creates the HTML for either the poll or the results depending
* on if the user has already voted
*/
public function __construct($params) {
$this->id = array_shift($params);
$this->question = array_shift($params);
$this->answers = $params;
$this->md5 = md5($this->id);
$this->header = str_replace('%src%', $_SERVER['SCRIPT_NAME'], $this->header);
$this->header = str_replace('%qid%', $this->md5, $this->header);
$this->header = str_replace('%question%', $this->question, $this->header);
# seperate cookie for each individual poll (has the user voted yet?)
# if cookie is set then show results(VOTES), if not then let user vote(POLL)
isset($_COOKIE[$this->md5]) ? $this->poll(self::VOTES) : $this->poll(self::POLL);
}
private function poll($show_poll) {
$replace_btn = $show_poll ? $this->button : '';
$get_results = webPoll::getData($this->md5);
$total_votes = array_sum($get_results);
$replace_votes = $show_poll ? $this->totalvotes : '<small>Total Votes: '.$total_votes.'</small>';
$this->footer = str_replace('%button%', $replace_btn, $this->footer);
$this->footer = str_replace('%totalvotes%', $replace_votes, $this->footer);
# static function doesn't have access to instance variable
if(!$show_poll) {
$results = webPoll::getData($this->md5);
$votes = array_sum($results);
}
for( $x=0; $x<count($this->answers); $x++ ) {
$this->center .= $show_poll ? $this->pollLine($x) : $this->voteLine($this->answers[$x],$results[$x],$votes);
}
echo $this->header, $this->center, $this->footer;
}
private function pollLine($x) {
isset($this->answers[$x+1]) ? $class = 'bordered' : $class = '';
return "
<li class='$class'>
<label class='poll_active'>
<input type='radio' name='AID' value='$x' />
{$this->answers[$x]}
</label>
</li>
";
}
private function voteLine($answer,$result,$votes) {
$result = isset($result) ? $result : 0;
$percent = round(($result/$votes)*100);
$width = $percent * $this->scale;
return "
<li>
<div class='result' style='width:{$width}px;'> </div>{$percent}%
<label class='poll_results'>
$answer
</label>
</li>
";
}
/**
* processes incoming votes. votes are identified in the database by a combination
* of the question's MD5 hash, and the answer # ( an int 0 or greater ).
*/
static function vote() {
if(!isset($_POST['QID']) || !isset($_POST['AID']) || isset($_COOKIE[$_POST['QID']])) {
# leave vote method if any of the above are true
return;
}
$dbh = new PDO('mysql:host=????;dbname=????', '????', '');
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
try {
# add vote info to 'tally' table
$sth = $dbh->prepare("INSERT INTO tally (QID,AID,votes,created_at) values (?, ?, 1, NOW())");
$ex = array($_POST['QID'],$_POST['AID']);
$sth->execute($ex);
# add ip info to 'ips' table
$sth2 = $dbh->prepare("INSERT INTO ips (ips,QID,AID) values (?,?,?)");
$ex2 = array($_SERVER['REMOTE_ADDR'],$_POST['QID'],$_POST['AID']);
$sth2->execute($ex2);
}
catch(PDOException $e) {
# 23000 error code means the key already exists, so UPDATE!
if($e->getCode() == 23000) {
try {
# update number of votes for answers in 'tally' table
$sth = $dbh->prepare("UPDATE tally SET votes = votes+1 WHERE QID=? AND AID=?");
$sth->execute(array($_POST['QID'],$_POST['AID']));
# add ip info to 'ips' table
$sth2 = $dbh->prepare("INSERT INTO ips (ips,QID,AID) values (?,?,?)");
$ex2 = array($_SERVER['REMOTE_ADDR'],$_POST['QID'],$_POST['AID']);
$sth2->execute($ex2);
}
catch(PDOException $e) {
webPoll::db_error($e->getMessage());
}
}
else {
webPoll::db_error($e->getMessage());
}
}
# entry in $_COOKIE to signify the user has voted, if he has
if($sth->rowCount() == 1) {
setcookie($_POST['QID'], 1, time()+60*60*24*365, '/', '', FALSE, TRUE);
$_COOKIE[$_POST['QID']] = 1;
}
}
static function getData($question_id) {
try {
$dbh = new PDO('mysql:host=????;dbname=????', '????', '');
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$STH = $dbh->prepare('SELECT AID, votes FROM tally WHERE QID = ?');
$STH->execute(array($question_id));
}
catch(PDOException $e) {
# Error getting data, just send empty data set
return array(0);
}
while($row = $STH->fetch()) {
$results[$row['AID']] = $row['votes'];
}
return $results;
}
/*
* You can do something with the error message if you like. Email yourself
* so you know something happened, or make an entry in a log
*/
static function db_error($error) {
echo "A database error has occurred. $error";
exit;
}
}
?>
And here's how the poll is implemented:
<?php
ini_set('display_errors',1);
error_reporting(E_ALL|E_STRICT);
include('webPoll-hiddenMD5.class.php');
webPoll::vote();
?>
<html>
<head>
<title>Poll Test</title>
<link rel="stylesheet" href="poll.css" type="text/css" />
<!--[if IE]>
<style> body { behavior: url("res/hover.htc"); } </style>
<![endif]-->
</head>
<body>
<?php
$a1 = new webPoll(array(
'March 16, 2012 What subjects would you like to learn more about?',
'What subjects would you like to learn more about?',
'HTML & CSS',
'Javascript',
'JS Frameworks (Jquery, etc)',
'Ruby/Ruby on Rails',
'PHP',
'mySQL'));
?>
</body>
</html>
Looks as if all your logic is in the constructor, simply do a query against the ip table for the users ip and qid and see if the number of results is greater then 0. If so then just change the last line of the constructor isset($_COOKIE[$this->md5] || count($res)>0 ) ? $this->poll(self::VOTES) : $this->poll(self::POLL);