[Question] Log in page's with Pdo

andylpsx

New Member
Messages
29
Reaction score
1
Points
3
I have been searching the internet for the last week for a tutorial that goes over how to make a registration and login system using PDO and Blowfish crypt methods. I have got to the point where I can crypt my passwords and store them in the database along with the username but the parts that I have been stuck at are preparing the database and pulling out he username and password and then comparing the password and allowing a person to log in. I have tried all that I could find like sqli real escape strings and bindparam and a few more. I can not get my prepare statements to return anything which makes me think that they are unable to pull from the database. If I could figure out how to pull from the database I would just need to find tutorials on comparing the password with the encrypted password. My main question, because I don't want to make anyone waste their time creating a tutorial, is where can I find a good tutorial that explains the steps. I want to understand what is happening not just do something but all the tutorials I find don't explain why they do things rather they just do it. I even tried to copy one exactly and it still did not work. I am still learning php and this is one of the things I've always wanted to learn, and I don't want to do it the lazy way but rather make sure the login page I make is secure. I tried using the PHP pdo Manuel pages as much as I could but I didn't get anywhere. Thanks for any help.
 

essellar

Community Advocate
Community Support
Messages
3,295
Reaction score
227
Points
63
I applaud your intentions. Getting this part of security right is far more important than most people realize.

For a big part of what you want to do (that is,make an actually secure login page), the "lazy way" (or rather "a way that may seem a bit too lazy") is the right way these days. And that's because PHP has added proper ways to do the password hashing and verification to the language. The built-in method uses a proper cryptographically-random salt (created using mcrypt_create_iv() and dev/urandom) and BCRYPT (the key stretcher/hash based on Blowfish) by default, so it's best practices all around, and as the situation changes, the defaults for the functions will change to become more secure over time. There is not only no need to roll your own anymore, it's actually gotten to the point that rolling your own will be setting yourself up for failure in the long run.

The new features are built into PHP 5.5.x. We'll be upgrading to that here in the not-too-distant future. In the meantime, there is a compatibility script that will bring the identical functions into PHP 5.4.x available here on github. (It's written by the same guy who wrote the core password functions for PHP 5.5, and is 100% compatible with the current defaults.) All you need to do is make a conditional require call:
PHP:
if (!function_exists("password_hash")) {
   require_once(<path to compatibility script>);
}

That way, when the server is upgraded to PHP 5.5 and beyond, you will be using what's built into the language rather than what's built into the compatibility script, getting the "free security upgrades" along the way. (The conditional require is to ensure that you don't try to redefine functions that already exist in the language.) You shouldn't have to revisit the code for many years.

The language functions (and the compatibility script) consists of a set of four functions: password_hash(); password_verify(); password_needs_rehash(); and password_get_info(). If you use the defaults (highly recommended, since you lose the auto-upgrade feature if you don't), you only need to use the first three (the server will use the password_get_info() code inside of password_needs_rehash(), but there's no reason for you to use it yourself).

To create a has of the user-supplied password (which you would do at registration, or for a password reset, or if the current hash needs rehashing), it's a simple matter of doing this:
PHP:
$hash = password_hash($password, PASSWORD_DEFAULT);

That will return an ugly string looking something like this: $2y$10$TR5/RZjbQuQNgwZqFcWuB.JBwefFGCPepA4G8H2rsyh9woEP8n6uC. The "$2y" at the front means "I used BCRYPT", the "$10" following that means "I used a cost factor of 10", the first few bytes of the rest (the computer knows which ones; you don't have to) is the salt, and the rest is the actual hash. So all of the information the sever needs to either re-do the hash the same way or to see if the hash method needs to be upadated is all stored in the hash string. That means that verifying the user password is as simple as this:
PHP:
if (password_verify($password, $hash_you_got_from_database) {
   // the user has supplied the correct password
}

If the user has successfully logged in, and while you still have the plain text password in memory, you can do this:
PHP:
if (password_needs_rehash($hash_you_got_from_database, PASSWORD_DEFAULT) {
   // the computer checks the algorithm and cost factor part of the hash
   $hash = password_hash($password, PASSWORD_DEFAULT);
   // update database with new hash...
}

That just leaves the database side of things.
 

essellar

Community Advocate
Community Support
Messages
3,295
Reaction score
227
Points
63
Now, most of the older tutorials out there are going to have you search the database for both the username and password. When you are using a salt, you can't do that. You won't know what the salt is (or what the hashing algorithm is, for that matter) until you have retrieved the hashed password from the database. All you have to work with, really, is the username (or email address, as the case may be). So your prepared statement is going to look something like this:
PHP:
$query = $PDO_object->prepare("SELECT user_id, username, password [,other stuff as required] FROM users WHERE email = :email");

I've used the email address to search on since it's guaranteed unique, but you can also use the username (or use both with an OR is your WHERE clause). The ":email" is a parameter, so to use this statement, you need to bind the user-supplied value to the parameter:
PHP:
$query->bindParam(":email", $email_value_from_user);

The bindParam() function does all of the necessary escaping, so you don't need to worry about invalid characters or SQL injection. (It does essentially what mysqli_escape_string and mysql_real_escape_string do.) When you bind the parameter, you can use the statement as if you had written it with the (escaped) value instead of the parameter. It's like using a variable in a string, except that is has all of the escaping functions and so forth built into it. Pretty straightforward, really, when you drop the idea that there is deep voodoo magic going on. Then it's just a matter of using the appropriate fetch_xxx() call to get the data from the database. (You may want to use an object, or perhaps an associative array. That's up to you and what's the most appropriate method for your code.)

There is a PDO tutorial here that is fairly understandable (it may take a couple of readings for things to click) and at least doesn't lie to you. With the password-handling part out of the way, you should be able to concentrate all of your effort on wrapping your head around PDO If you have any questions, please feel free to ask. Again and again, if necessary -- the problem with people who know stuff is that sometimes they forget what it's like NOT to know stuff, so if the "simple explanation" went over your head, it may have been because it wasn't simple enough.
 

Skizzerz

Contributors
Staff member
Contributors
Messages
2,928
Reaction score
118
Points
63
Please note that if you pass the user's password to password_hash() as-is, it is limited to 72 bytes (this is a lesser-known limitation of the blowfish algorithm that bcrypt uses - I arrived at the 72 byte limit in my own test scripts as well, one on PHP 5.4 using the compatibility library, and one on PHP 5.5 using the native functions). I recommend hashing the password with something like sha-512 and then passing the result of that to password_hash in order to allow for longer passwords. For example:
PHP:
// the inner hash function outputs 64 bytes of binary data, which is within the limit of 72
// we output in binary mode so that we do not lose any entropy due to converting to hexadecimal digits
$hash = password_hash( hash( 'sha512', $password, true ), PASSWORD_DEFAULT );

EDIT: For those curious, this was the test script I was using so you can verify for yourself that there is in fact a character limit on passwords. Basically it hashes a particular password, then checks if that password + one random character hashes to the same value. You can re-run multiple times to get different random values to see the results are consistent.
PHP:
<?php

$password = chr( rand( 0, 255 ) );
for ($i = 1; $i <= 256; $i++) {
	$next = $password . chr( rand( 0, 255 ) );
	$hash = password_hash( $password, PASSWORD_BCRYPT );
	if ( password_verify( $next, $hash ) ) {
		echo 'Maximum length is ' . $i;
		exit;
	}
	$password = $next;
}
echo 'No maximum found';

EDIT 2: Modified original example to use sha-512 in binary mode, which is still within the 72-byte limit and provides more entropy than sha-256 in text/hexadecimal characters mode
 
Last edited:

essellar

Community Advocate
Community Support
Messages
3,295
Reaction score
227
Points
63
Indeed. I've used SHA512 (previously SHA256) in my own code, and mentioned it here before in one thread or another. Passwords should not have an upper bound. Lower, yes.

It does need to be emphasized, though, that while this is a practical work-around for the BCRYPT length limit, it does not actually gain you anything in security terms (in that anyone who uses a password/passphrase of more than 64 bytes is eating into the password space at and below 64 bytes). You lose 8 bits of entropy (which, at this level, might not be tragic). It makes it slightly more likely that an individual account (not password) can be cracked since it increases the possible number of collisions. (That is, a cracker can find a value that is not your actual password, but that hashes to the same value as your password. That said, it is only transferable to other places if SHA512 is used to pre-process passwords there as well. Getting around that means generating and storing another salt (please use mcrypt_create_iv with MCRYPT_DEVURANDOM if you do).

The whole point of getting security as close to right as possible is to protect your users, not your site. Even when our own sites are relatively meaningless in the larger sense of things, we need to keep in mind that our users are probably using the same one or two passwords everywhere they need a password. Someone breaking in to spam or troll your wonderful little community isn't nearly as important as keeping the same cracker out of your users' email, bank accounts, health records, etc.
 
Last edited:

ddnhf43

New Member
Messages
19
Reaction score
0
Points
1
Hello, I'm Jonathan and I'll be attempting to help you with the login script for you today, I will be using comments and attempting to explain the code in order to insure you're understanding what we're doing completely.

First off, we need to initiate the database connection:
PHP:
    <?php
    $host = "127.0.0.1" // Use 127.0.0.1 instead of localhost to make database connection faster
    $dbname = "database name"; // This is your database name
    $dbuser = "database user"; // This is your database user
    $dbpass = "password"; // This is your database password

    try {
        $con = new PDO('mysql:host=' . $host . ';dbname=' . $dbname, $dbuser, $dbpass);
        $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    } catch(PDOException $e) {
        die($e->getMessage());
    }
    ?>
What we just did is initiated the database connection, now it may seem overwhelming but it's actually very easy once you get into it. We assigned the variable "$con" to the PDO database connection class in which creates the database connection. Where we use the method "setAttribute()" we are basically just telling PHP what type of error we want. We have to surround the connection in a try/catch block because if there is an error connecting, it throws an exception which if uncaught, can create more errors.

Now, we are going to jump to the login script, I'm assuming you're using username/password and possibly "remember me". We don't to use blowfish to hash the passwords, but for the sake of this. I'm going to use SHA1.
PHP:
    <?php 
    $username = $_POST['username']; // Assigning the submitted username to a variable
    $password = sha1($_POST['password']); // Assigning the submitted password to a variable

    if(isset($username) && isset($password)) {
        $user = $con->prepare("SELECT username FROM users WHERE username = :user AND password = :pass");
        $user->execute(array(
            ":user" => $username,
            ":pass" => $password
        ));

        if($user->rowCount() == 1) {
            while($get = $user->fetch(PDO::FETCH_OBJ)) {
                if(!$_POST['rememberme']) {
                    $_SESSION['username'] = $get->username;
                    $_SESSION['id'] = $get->id;
                    $_SESSION['loggedin'] = 1;

                    echo 'Logged in!';
                } else {
                    // Set  the cookies, using the values for the sessions
                }
            }
        } else {
            echo 'User does not exist!';
        }
    } else {
        echo 'Please fill in all provided fields!';
    }
    ?>
What we did there was just comparing the submitted info from the user logging in to the records in the database, and preforming some checks to see if the user exists and to see if the data has been submitted.

I hope this helped, I know I could of done better with the explaining but I did the best I could.
 

leafypiggy

Manager of Pens and Office Supplies
Staff member
Messages
3,819
Reaction score
163
Points
63
No. Don't use sha1. Use built in functions for password hashing.
 

Skizzerz

Contributors
Staff member
Contributors
Messages
2,928
Reaction score
118
Points
63
Using password_hash() doesn't add any additional time over using sha1. I recommend that you edit your post so that it uses a better hashing mechanism as mentioned in previous comments here, as one of the major downfalls of PHP is people finding sites like this and blindly copy/pasting the code without both a) reading the context of the thread, and b) bothering to see what the code actually does.

Thus, any code you paste (even if it is just "sample code") should take all of the necessary precautions you would take if you were running it on a production server for your company or whatnot.
 
Top