CUTCODEDOWN
Minimalist Semantic Markup

Welcome Guest
Please Login or Register

If you have registered but not recieved your activation e-mail in a reasonable amount of time, or have issues with using the registration form, please use our Contact Form for assistance. Include both your username and the e-mail you tried to register with.

Author Topic: Handling User Passwords  (Read 4176 times)

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Handling User Passwords
« on: 26 Jan 2020, 11:01:36 am »
Hello again its the resident forum PITA.

This time its regarding user passwords and logins.

Should this all be done via password_hash and password_verify?

I personally think its a bad idea to call the password hash out the database to verify when it would be better served being put into the where clause on the query? But I am no professional and that can be clearly spotted by my posts... ...but as I say I am trying to learn from you all - and hopefully that can also be spotted... :)

« Last Edit: 26 Jan 2020, 05:49:12 pm by GrumpyYoungMan »
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #1 on: 26 Jan 2020, 03:28:36 pm »
Also when it comes to the database connection (PDO) is it best to keep that secured within a function? and not just generally connect and pass it around via a variable such as $DB?

What is best practice? 
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

Jason Knight

  • Administrator
  • Hero Member
  • *****
  • Posts: 1054
  • Karma: +188/-1
    • CutCodeDown -- Minimalist Semantic Markup
Re: Handling User Passwords
« Reply #2 on: 27 Jan 2020, 04:58:32 am »
There WAS a lot of debate a number of years ago about password_hash and the bad practice of pulling the password -- even hashed -- from the database to be checked instead of keeping all password access mono-directional. Those who opposed it were shouted down and kicked out of contributing to PHP's future.

I still maintain that pulling the hash from the database is just plain stupid. Hence my approach remains to use the hash() command with something like sha256 if speed is a concern, sha512 or whirlpool if I want the best of the best of the best. You want to test for login, you hash the pass and test for equality in the query.

I also dislike how password_hash plays loosy-goosy mix and match of password formats, defeating any point of "hey don't use that algo anymore" that it ALLEGEDLY brings to the table. That a new algo isn't blanket applied or warnings aren't thrown to say to do so means you're still stuck manually changing algo's, at which point why bother with these functions in the first place?

Also when it comes to the database connection (PDO) is it best to keep that secured within a function? and not just generally connect and pass it around via a variable such as $DB?
Uhm, those are the same thing in terms of good practice -- you connect to it locally, then you pass $db only to FUNCTIONS that require its use. This is part of why library files should contain functions and NOT directly execute code. (Something I wish they'd drop from being a thing in PHP altogether is that includes are in scope and directly execute code!)

Hence my programs usually have a "main" function in their "one index to rule them all"

function main() does the following:

$db = connect to database
test for / log in user, passing $db to user object
load settings from passing $db to settings singleton
include the template
determine the user request type
include request handler, passing $db to request handler object/singleton
// do not pass $db past this point. Various PDOStatements would hold the results
template_header();
run request handler output function/object method which in turn calls its template
template_footer();
die(); // prevent further code execution, makes code appendages on index be dead code.

Then I run main() right after it is created.

This gives me granular control over what has access to the database and what does not, and since I write single index.php to handle all requests it gives me a single entry vector. Even if accidentally included/requried or run directly, NONE of my other library files can give away or do anything nasty as all they contain are functions and objects.

To me that's good practice. To folks who like things like turdpress that's "too hard" or "too restrictive" or "too smart" for them; what with their putting the security information for the database connection into DEFINE; super-global scope where they cannot be deleted, and hence 100% herpafreakingderp. I've seen so many PHP tutorials even advocating that approach and it's like "what the **** is wrong with you people?!?"

One of the BIGGEST security flaws in PHP architecturally is how includes are blindly just read in and run, meaning the language has no proper library model. Includes are automatically in scope to where they are called, they will just run the code included with zero concern for security be they included or called directly, it's a sloppy implementation at best.

Hence I even use this derpy routine:

Code: [Select]
function safeInclude($file) { include($file); }
For including libraries JUST so I can break scope -- the scope of the include becomes safeInclude instead.

Because the fact I can do this:

Code: [Select]
<?php
// test.php
$test 'stupid';
include(
'stupid.php');

Code: [Select]
<?php
// stupid.php
echo $test;

... and it will work? That's "scope bleed" and it's absofragginglutely dumbass.

But again, my bread and butter for a decade was programming Ada for the fed. It gives me a very different outlook on what's secure and what's not.

Hence my own laundry list for "fixing" PHP's security woes?

1) remove all PHP shorttags. If it's .php it contains PHP code. No more of this willy-nilly <?php ?> rubbish or letting markup be interspersed without a direct echo command.

2) library files included or required can/should only contain functions or objects. No more "blind output" from an include.

3) stop allowing file functions like file_get_contents, readfile, fopen, and so forth to read or write to PHP includes / library files.

4) ditch double-quoted string support as well as confusing trash like heredoc/nowdoc. Not only are they less efficient, they're harder to read and often seem to cause either variables for nothing if proper escaping (htmlspecialchars for example) is done, or making people not even bother escaping opening up security holes.

But when I proposed these fixes over a decade ago, I got yelled at. THEN people wonder why I have an attitude about this ****!
We are all, we are all, we are all FRIENDS! For today we're all brothers, tonight we're all friends. Our moment of peace in a war that never ends.

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #3 on: 27 Jan 2020, 06:01:27 am »
Jason I like your spade is a spade approach - and that is why I post here - so don't go changing.

I am doing my day job now, so will digest that fully later on!

Thanks for all your input and help - I hope you can see I am taking notice and changing - very slowly...
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #4 on: 28 Jan 2020, 08:40:40 am »
For now I am using password_verify:

Code: [Select]
// If the "username" and "password" inputs are set then continue with the login:
    if( $_POST['username'] != "" && $_POST['password'] != "" ) {

        // Fetch User from DB
        $fetchUser = $DB->prepare("
                            SELECT
                                u_id, u_user, u_email, u_password, u_login_attempts
                            FROM   
                                {$prefix}users
                            WHERE
                                u_user=:username                               
                            ");

        $fetchUser->execute( [
                    ':username' => $_POST['username'],
                        ] );

        $dbUser = $fetchUser->fetch();

        // PASSword Match?   
        if( ! empty($dbUser) ) {
           
            if( $dbUser['u_login_attempts'] < 3 ) {
           
                if( ! password_verify($_POST['password'], $dbUser['u_password'] ) ) {

                    // Incorrect Password:
                    $errors[] = "invalid_login_2_credentials";               

                    // Update FAILED logim attempts:
                    $userLogInAttempts = $DB->prepare("
                                                UPDATE
                                                    {$prefix}users
                                                SET   
                                                    u_login_attempts = Coalesce(u_login_attempts, 0)+1, u_last_login_attempt = NOW()
                                                WHERE
                                                    u_id=:uid
                                                ");

                    $userLogInAttempts->execute( [
                                            ':uid' => $dbUser['u_id'],
                                            ] );

                    if ( ( $dbUser['u_login_attempts'] + 1 ) >= 3 ) {

                        // ADD EMAIL TO QUEUE
                        $lockedEmail = $DB->prepare("
                                INSERT INTO
                                    {$prefix}email_queue (
                                        eq_id, eq_to, eq_from, eq_subject, eq_message, eq_added
                                    )
                                VALUES (
                                    :id, :to, :from, :subject, :message, NOW()
                                    )
                            ");

                        $lockedEmail->execute( [
                            ':id' => uniqid(),
                            ':to' => $dbUser['u_email'],
                            ':from' => $CONFIG['email_from'],
                            ':subject' => "Account Locked",
                            ':message' =>"Your account has now been locked for security reasons. <p>Sorry!</p>Test Message!"
                            ] );
                    }

                }
                else {

                    echo "<h1>MORE login stuff...?</h1>\n";

                    // Update Last Login:
                    $userLastLogIn = $DB->prepare("
                                            UPDATE
                                                {$prefix}users
                                            SET   
                                                u_last_login = NOW()
                                            WHERE
                                                u_id=:uid
                                                ");

                    $userLastLogIn->execute( [
                                        ':uid' => $dbUser['u_id'],
                                            ] );

                }
            }
            else {

                // LOCKED Account:
                $errors[] = "invalid_login_3_credentials";

            }

        }
        else {

            // Invalid DB User
            $errors[] = "invalid_login_1_credentials";

        }

Am I going down the right lines? Securely? apart from the database hash retrieve... I am not a fan of this...
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

Jason Knight

  • Administrator
  • Hero Member
  • *****
  • Posts: 1054
  • Karma: +188/-1
    • CutCodeDown -- Minimalist Semantic Markup
Re: Handling User Passwords
« Reply #5 on: 28 Jan 2020, 02:26:37 pm »
First off never blindly !='' on $_POST or any other system set array, use !empty instead in case the index doesn't exist.

Next, stop using double quotes where you don't need them. If you do need them, use string addition as it's faster than string parsing.

Use "if on assignment" rather than screwing around assigning then testing.

I would add an expiry time to the lockout. Hard lockouts with NO timeout could screw even yourself. Usually if you shut them out for 24 hours that's sufficient.

!empty on the $dbUser fetch is unneccessary, any loose false will do.

One big "screwup" (IMHO) is how you check for failed login / lockout twice. I'd try re-arranging that, but for now I lack the time to really go in and do a rewrite to show what I mean.

For the most part though you're on the right track, you just need some tweaking and better practices.
We are all, we are all, we are all FRIENDS! For today we're all brothers, tonight we're all friends. Our moment of peace in a war that never ends.

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #6 on: 28 Jan 2020, 03:51:26 pm »
Thanks I will see if I can figure it all out...
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #7 on: 28 Jan 2020, 05:45:24 pm »
Well I am off to bed - this has me beat, I  can not see how I can lock the account without doing the two tests on the login attempts.

I know I can alter the query to make it only query when the login attempts is less than 3? Or is that what you meant?

I have changed one part to:
Code: [Select]
// If the "username" and "password" inputs are set then continue with the login:
if( ! empty($_POST['username'] ) && ! empty ($_POST['password'] ) ) {

For some reason I thought empty was for whole arrays only and not keys - I have a lot of reading to do...
« Last Edit: 29 Jan 2020, 12:53:25 am by GrumpyYoungMan »
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #8 on: 29 Jan 2020, 04:13:30 am »
Something like this:
Code: [Select]
$errors = [];
    // This can and will be removed -  I'll do that soon!
    $reqIns = [ 'username', 'password' ];

    foreach( $reqIns as $reqIn ) {

        if( empty($reqIn) ) { $errors[] = "missing_{$reqIn}_input"; }

    }

    // If the "username" and "password" inputs are set then continue with the login:
    if( ! empty($_POST['username'] ) && ! empty ($_POST['password'] ) ) {

        // Fetch User from DB
        $fetchUser = $DB->prepare("
                            SELECT
                                u_id, u_user, u_email, u_password, u_login_attempts
                            FROM   
                                {$prefix}users
                            WHERE
                                u_user=:username
                            AND
                                u_locked != 1                           
                            ");

        $fetchUser->execute( [
                    ':username' => $_POST['username'],
                        ] );

        $dbUser = $fetchUser->fetch();

if ( false !== $dbUser ) {

    if( ! password_verify($_POST['password'], $dbUser['u_password'] ) ) {

        $errors[] = "invalid_login_2_credentials";

        if( $dbUser['u_login_attempts'] >= 0 && $dbUser['u_login_attempts'] < $maxLogInAttempts ) {

            // Update FAILED logim attempts:
            $userLogInAttempts = $DB->prepare("
                                        UPDATE
                                            {$prefix}users
                                        SET   
                                            u_login_attempts = Coalesce(u_login_attempts, 0)+1, u_last_login_attempt = NOW()
                                        WHERE
                                            u_id=:uid
                                        ");

            $userLogInAttempts->execute( [
                                ':uid' => $dbUser['u_id'],
                                ] );

        }
        else {

            // Incorrect PASSWORD:           
           

            // Update FAILED login attempts:
            $userLogInAttempts = $DB->prepare("
                                        UPDATE
                                            {$prefix}users
                                        SET   
                                            u_login_attempts = Coalesce(u_login_attempts, 0)+1, u_last_login_attempt = NOW(), u_locked=1
                                        WHERE
                                            u_id=:uid
                                        ");

            $userLogInAttempts->execute( [
                                ':uid' => $dbUser['u_id'],
                                    ] );

            // ADD EMAIL TO QUEUE
            $lockedEmail = $DB->prepare("
                                INSERT INTO
                                    {$prefix}email_queue (
                                        eq_id, eq_to, eq_from, eq_subject, eq_message, eq_added
                                    )
                                VALUES (
                                    :id, :to, :from, :subject, :message, NOW()
                                    )
                            ");

            $lockedEmail->execute( [
                            ':id' => uniqid(),
                            ':to' => $dbUser['u_email'],
                            ':from' => $CONFIG['email_from'],
                            ':subject' => "Account Locked",
                            ':message' =>"Your account has now been locked for security reasons. <p>Sorry!</p>Test Message!"
                            ] );

        }
   
    }
    else {
       
        // Login Successful:

                    // Update Last Login:
                    $userLastLogIn = $DB->prepare("
                                            UPDATE
                                                {$prefix}users
                                            SET   
                                                u_last_login = NOW()
                                            WHERE
                                                u_id=:uid
                                                ");

                    $userLastLogIn->execute( [
                                        ':uid' => $dbUser['u_id'],
                                            ] );

       
    }

}
else {

    // Invalid DB User
    $errors[] = "invalid_login_1_credentials";

}

I am off to do my day job now - one thing I need to alter, is to make it look for the locked account after the query so I can alter the message? I thought we need to keep the messages vague to stop the hackers?

I will also add a time out query to the site to auto-unlock the account.

Thanks again for the feedback Jason...
« Last Edit: 29 Jan 2020, 05:24:30 am by GrumpyYoungMan »
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #9 on: 29 Jan 2020, 06:06:54 am »
When it comes to handling the user_id - as in a logged in user, all guests will be either user_id 0 or they wont even have a user_id, is it best to just use the $_SESSION super global to hold the user_id or create a session table in the database? or is that old bad practice now?

Oh:
Code: [Select]
$errors = [];

    // If the "username" and "password" inputs are set then continue with the login:
    if( ! empty($_POST['username'] ) && ! empty ($_POST['password'] ) ) {

        // Fetch User from DB
        $fetchUser = $DB->prepare("
                            SELECT
                                u_id, u_user, u_email, u_password, u_login_attempts, u_locked
                            FROM   
                                {$prefix}users
                            WHERE
                                u_user=:username                                                       
                            ");

        $fetchUser->execute( [
                    ':username' => $_POST['username'],
                        ] );

        $dbUser = $fetchUser->fetch();

        if ( false !== $dbUser ) {

            // Is the account locked:
            if( 1 !== $dbUser['u_locked'] ) {

                if( ! password_verify($_POST['password'], $dbUser['u_password'] ) ) {

                    $errors[] = "invalid_login_2_credentials";

                        if( $dbUser['u_login_attempts'] >= 0 && $dbUser['u_login_attempts'] < $maxLogInAttempts ) {

                            // Update FAILED logim attempts:
                            $userLogInAttempts = $DB->prepare("
                                                        UPDATE
                                                            {$prefix}users
                                                        SET   
                                                            u_login_attempts = Coalesce(u_login_attempts, 0)+1, u_last_login_attempt = NOW()
                                                        WHERE
                                                            u_id=:uid
                                                    ");

                            $userLogInAttempts->execute( [
                                                ':uid' => $dbUser['u_id'],
                                                    ] );

                        }
                        else {

                            // Incorrect PASSWORD:           
                            // Update FAILED login attempts:

                            $userLogInAttempts = $DB->prepare("
                                                        UPDATE
                                                            {$prefix}users
                                                        SET   
                                                            u_login_attempts = Coalesce(u_login_attempts, 0)+1, u_last_login_attempt = NOW(), u_locked=1
                                                        WHERE
                                                            u_id=:uid
                                                        ");

                            $userLogInAttempts->execute( [
                                                ':uid' => $dbUser['u_id'],
                                                    ] );

                            // ADD EMAIL TO QUEUE
                            $lockedEmail = $DB->prepare("
                                                    INSERT INTO
                                                        {$prefix}email_queue (
                                                            eq_id, eq_to, eq_from, eq_subject, eq_message, eq_added
                                                                            )
                                                    VALUES (
                                                        :id, :to, :from, :subject, :message, NOW()
                                                        )
                                                ");

                            $lockedEmail->execute( [
                                            ':id' => uniqid(),
                                            ':to' => $dbUser['u_email'],
                                            ':from' => $CONFIG['email_from'],
                                            ':subject' => "Account Locked",
                                            ':message' =>"Your account has now been locked for security reasons. <p>Sorry!</p>Test Message!"
                                            ] );

                        }
   
                }
                else {
       
                        // Login Successful:

                        // Update Last Login:
                        $userLastLogIn = $DB->prepare("
                                                UPDATE
                                                    {$prefix}users
                                                SET   
                                                    u_last_login = NOW()
                                                WHERE
                                                    u_id=:uid
                                                ");

                        $userLastLogIn->execute( [
                                            ':uid' => $dbUser['u_id'],
                                                ] );

       
                }

            }
            else {

                $errors[] = 'invalid_login_3_credentials';

            }

        }
        else {

            // Invalid DB User
            $errors[] = "invalid_login_1_credentials";

        }

}
else { $errors[] = "invalid_login_empty_credentials"; }

    // Return to from if we have errors:
    if( count($errors) > 0 ) {
       
        login_ShowForm();

    }
^ Tweaked it some more...
« Last Edit: 29 Jan 2020, 08:13:39 am by GrumpyYoungMan »
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

Jason Knight

  • Administrator
  • Hero Member
  • *****
  • Posts: 1054
  • Karma: +188/-1
    • CutCodeDown -- Minimalist Semantic Markup
Re: Handling User Passwords
« Reply #10 on: 29 Jan 2020, 11:39:15 am »
Making your own session tables can give you a degree of finer control over things like expiry, but in most cases it's just not worth the hassle. I used to be an adherent to doing that, but then I learned how to edit my php.ini files properly and stopped using hosts that don't let me do that.

Working for others where often they are on hosts that don't let me set things like the default session times, I'll code it in as an OPTION... but for the most part if you're on a decent host where you can configure things properly there's no reason to waste your time on it.
 
I'd classify database driven sessions as something to learn how to do, but not necessarily something one should be deploying if you have quality hosting. It's a kludge for garbage hosts.
We are all, we are all, we are all FRIENDS! For today we're all brothers, tonight we're all friends. Our moment of peace in a war that never ends.

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Re: Handling User Passwords
« Reply #11 on: 29 Jan 2020, 12:06:10 pm »
I have my own server, so I can do what I like, I guess...

Is the code that bad you can't believe it? or is it more or less where it needs to be - I have made a few tweaks since it was posted, but won't post it up again just yet...
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
Password History
« Reply #12 on: 8 Feb 2020, 04:38:12 am »
Is it a good idea/advisable to keep a history of old user passwords so they can not reuse an old password on password change request? if it is how many changes should be stored? the last 10?

Obviously I know to store the hash only and not the raw password, as that shouldn't be stored ANYWHERE online - only the hash.

Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

GrumpyYoungMan

  • Hero Member
  • *****
  • Posts: 792
  • Karma: +8/-0
    • Grumpy Young Man
PassWord History
« Reply #13 on: 8 Feb 2020, 08:43:01 am »
Password Check:
Code: [Select]
$errors = [];

// Password History Check:
$checkPassword = $DB->prepare("
SELECT
    password
   FROM
    {$DB->CONFIG['sql_tbl_prefix']}expired_passwords
   WHERE
    user=:uid
   ORDER BY
    date_added
   LIMIT
    10
");

$checkPassword->execute([ 'uid' => $USER['id'] ] );

while( $r = $checkPassword->fetch() ) {

// If we find a match, log it and break the loop...
if( password_verify($_POST['new_password'], $r['password']) ) { $errors[] = 'used_password_before'; break; };

}


return $errors;
Trying to learn a new trick to prove old dogs can learn new ones...

Total Novice have-a go Amateur Programmer - not sure that is the right thing to say... but trying to learn...

Dave

  • Junior Member
  • *
  • Posts: 38
  • Karma: +12/-0
Re: Password History
« Reply #14 on: 8 Feb 2020, 12:06:22 pm »
Is it a good idea/advisable to keep a history of old user passwords so they can not reuse an old password on password change request?

Please don't even think about it! As I said in another thread, you aren't my mother so let me make choices good or bad.

I can't tell you how frustrating it is to come across a site that does something like that. It's even worse where I work where they disallow ANY password you've ever used. I swear just a little bit every time I have to change that password.
Dave

 

SMF spam blocked by CleanTalk

Advertisement