As a self-taught PHP user, I see that I shouldn't rely on my marginal knowledge in security questions, so I set out and browsed the internet on how to design an authentication system that works on more than a single page. I found quite a few results, actually, but all of them were many years old, so I ask whether anyone has a tutorial or a good example on how I might do it...
Sorry for the bump, but I am doing this for a friend who's paying the webspace, domain and all that stuff all this time... Could someone point me at a general direction so I could try to figure it out?
Nik wrote:
Sorry for the bump, but I am doing this for a friend who's paying the webspace, domain and all that stuff all this time... Could someone point me at a general direction so I could try to figure it out?
Use cookies for sessions. Or perhaps use an existing library for authentication; these are usually very secure and well-tested.
This old topic of mine might be a good place to start. I failed to really bump it when I was successful but I'll go ahead and post that stuff here. Basically, look into sessions, once you learn how to open and close a session, it should all be pretty straight forward. I'm sorry I can't offer solid help so here are some copy and pastes from when I last had to do this back in 2014.
My authentication and session all happens on one page but I imagine it shouldn't be too hard to have this persist across multiple pages. Here is my index.php where you can see that everything happens on this one page.
Code:
Here is my header file. This stuff should be well documented but feel free to ask if you have any questions.
Code:
Not nearly as important, the footer. I'm including it here because that's where I display my "logout" link if the user is logged in.
Code:
The login form is basically this. I can't offer much commentary on this other than the form reloads the page it's on.
Code:
I snipped a bunch of info out of my header.php and won't directly paste "loggingin_body2.php" which does the cross checking and stuff. But if you're curious, that's where the error generation happens to and I will paste that.
Code:
Hopefully this helps. Again, sorry I can't provide actual help and point you where to go. This was so long ago that I myself am kinda fuzzy on the process and the resources I used. I'm thankful that I comment the heck out of my code because if I didn't, I'd be so confused when I go back to change something.
My authentication and session all happens on one page but I imagine it shouldn't be too hard to have this persist across multiple pages. Here is my index.php where you can see that everything happens on this one page.
Code:
index.php
<?php
include_once('res/inc/header.php');
// Let's show any generated errors.
error_reporting(E_ALL);
ini_set('display_errors', true);
// Here we begin the page content
// The visitor has a valid session, let's get the required info
if(isset($_SESSION['user'])) {
include_once('res/inc/loggedin_body.php');
} elseif(isset($_POST['login'])) {
// The visitor has sent the form, let's see their credentials are valid
include_once('res/inc/loggingin_body2.php');
} else {
// Since we aren't logged in, let's prompt the visitor to login
include_once('res/inc/login_form.php');
}
include_once('res/inc/footer.php');
?>
Here is my header file. This stuff should be well documented but feel free to ask if you have any questions.
Code:
header.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', true);
// Starting the session
session_start();
// Get the user agent for failed attempts.
$uagent = $_SERVER['HTTP_USER_AGENT'];
// Checking to see if the login form was submitted
if(isset($_POST['login'])) {
// Refresh the page immediately
// Yes, the code will execute fast enough
header("refresh: 0;");
}
// If the user wants to sign out, perform the following
if(isset($_GET['logout'])) {
// Unset and destroy the session, this way when the user comes back to this page they will be prompted to log in.
session_unset();
session_destroy();
// Refresh the page so they are redirected away from their active session.
header("refresh: 0; url=/");
}
Not nearly as important, the footer. I'm including it here because that's where I display my "logout" link if the user is logged in.
Code:
footer.php
<hr>
<p class="small">Alex Glanville 2014 | <?php if(isset($_SESSION["user"])) {echo '| <a href="/?logout">Logout</a>'; } ?></i></p>
</div>
</body>
</html>
The login form is basically this. I can't offer much commentary on this other than the form reloads the page it's on.
Code:
login_form.php
<?php
echo '<h1>Client Login</h1>';
// Was the last attempt a failed one?
if(isset($_SESSION['fail'])) {
echo $_SESSION['fail'];
}
// Contine with the form!
echo '
<form method="POST" action="/">
User <br>
<input type="text" name="user" size="40"><br>
Password <br>
<input type="password" name="pass" size="40"><br>
<input id="button" type="submit" name="login" value="login">
</form> ';
session_unset();
session_destroy();
?>
I snipped a bunch of info out of my header.php and won't directly paste "loggingin_body2.php" which does the cross checking and stuff. But if you're curious, that's where the error generation happens to and I will paste that.
Code:
logginin_body2.php
// Parse and santize the username and password into a variable
// We sanitize to prevent SQL injections.
// I hear MySQLi is rather primitive.
$user = preg_replace("/[^A-Za-z0-9 ]/", '', htmlspecialchars($_POST['user']));
$pass = preg_replace("/[^A-Za-z0-9 ]/", '', htmlspecialchars($_POST['pass']));
// Error Messages
## General failure
$fail = 'Invalid Credentials, please try again!';
## Too many login attempts
$many = "You have tried unsuccessfully too many times.<br>Please wait $mins minutes before trying again.";
## Nothing submitted
$nothing = "You have left out information!<br>Please type in your username and a password.";
[...]
// If user still has content
if($user == $found_user) {
# echo "Found user!<br>";
// Check password against the database result
// If it matches, log in. Else try again.
[....]
//Set the session and refresh the page!
$_SESSION["user"] = $user;
$_SESSION["account"] = $url;
# echo "Password Checks out</br>";
} else {
// The password was wrong.
$_SESSION['fail'] = $fail;
[....]
}
} else {
// The username was wrong.
$_SESSION['fail'] = $fail;
}
Hopefully this helps. Again, sorry I can't provide actual help and point you where to go. This was so long ago that I myself am kinda fuzzy on the process and the resources I used. I'm thankful that I comment the heck out of my code because if I didn't, I'd be so confused when I go back to change something.
From the information I gathered, I think I came up with a basic idea on how I could do this...
When a user opens the page, I check whether they have a cookie "Session" and there is a database entry for the user. If either is missing, they're prompted for a password, authenticate and get sent a random key, which is stored in "Session". "Session" has a lifetime of 30 minutes. Server side, the key is hashed and stored in a database, set to expire with the cookie.
If the user had "Session" set already and there is a database entry for the username, the cookie is hashed and compared against the database. If they match, the user gets automatically authenticated. The key is deleted, a new one generated, sent to the user, hashed and stored on the server.
Is that safe? Are there any obvious exploits or anything I need to be careful with?
When a user opens the page, I check whether they have a cookie "Session" and there is a database entry for the user. If either is missing, they're prompted for a password, authenticate and get sent a random key, which is stored in "Session". "Session" has a lifetime of 30 minutes. Server side, the key is hashed and stored in a database, set to expire with the cookie.
If the user had "Session" set already and there is a database entry for the username, the cookie is hashed and compared against the database. If they match, the user gets automatically authenticated. The key is deleted, a new one generated, sent to the user, hashed and stored on the server.
Is that safe? Are there any obvious exploits or anything I need to be careful with?
Sounds like a pretty typical design. The devil with these sorts of things is in the implementation details.
You also need to be able to revoke session keys, for example if a user changes their password because somebody learned it and the third party was able to create a session as that user- on password change (and possibly in other situations) you need to be able to revoke all session keys for that user.
Quote:
the key is hashed and stored in a database, set to expire with the cookie.
How do you ensure the server expires a cookie on time? If it doesn't, a cookie that is kept too long (for example because somebody's browser doesn't honor cookie expiration times) can wrongly allow access to a stale session.
You also need to be able to revoke session keys, for example if a user changes their password because somebody learned it and the third party was able to create a session as that user- on password change (and possibly in other situations) you need to be able to revoke all session keys for that user.
Tari wrote:
How do you ensure the server expires a cookie on time? If it doesn't, a cookie that is kept too long (for example because somebody's browser doesn't honor cookie expiration times) can wrongly allow access to a stale session.
That's why I set a time limit for cookies client side and one on the database server side. Every time a key is queried in the database, it is deleted and ignored if it has already expired. Additionally, I will set up a cronjob to sanitize the database once in a while.
EDIT: I misunderstood that at first. Now I realize what you mean. Probably the key will have, in addition to the things above, the username appended? To prevent duplicate use of this kind of expired keys. </edit>
What I just realized could happen, though, is - how do I make sure the random keys don't duplicate accidentally and I end up with two sessions with the same key? Can I solve this by querying the database for that key before confirming it and proceeding with the session?
Tari wrote:
You also need to be able to revoke session keys, for example if a user changes their password because somebody learned it and the third party was able to create a session as that user- on password change (and possibly in other situations) you need to be able to revoke all session keys for that user.
The session keys are supposed to be very short lived. They would change every time the user loads a page (I close the old session and start a new one), and if the user is inactive for half an hour, the key expires on its own. To "Remember" a user, I might indeed just increase the living time of the key, and delete them on appropriate occasions such as you suggest, the same way I delete normal invalid keys.
Tari wrote:
Sounds like a pretty typical design. The devil with these sorts of things is in the implementation details.
Is that a general statement or are there any special parts which need to be secured? Or did you name them all already?
Nik wrote:
What I just realized could happen, though, is - how do I make sure the random keys don't duplicate accidentally and I end up with two sessions with the same key? Can I solve this by querying the database for that key before confirming it and proceeding with the session?
Doesn't matter how you do it, as long as it's not possible for a user to be granted access to somebody else's session by randomly being assigned the same session ID.
If you don't want to pass in an expected user ID, checking that a new ID doesn't collide with an existing one before using it is fine but requires more database queries.
Quote:
Tari wrote:
Sounds like a pretty typical design. The devil with these sorts of things is in the implementation details.
Is that a general statement or are there any special parts which need to be secured? Or did you name them all already?
Keep in mind the context though. If you're building a little web site that doesn't do anything important, maybe you can afford to be less careful. At the same time, if you're trying to learn it may be instructive to be as robust as possible.
Tari wrote:
If you don't want to pass in an expected user ID, checking that a new ID doesn't collide with an existing one before using it is fine but requires more database queries.
I am not sure I got this - what do you mean by passing an expected ID?
Tari wrote:
Keep in mind the context though. If you're building a little web site that doesn't do anything important, maybe you can afford to be less careful. At the same time, if you're trying to learn it may be instructive to be as robust as possible.
Actually, this is supposed to be a webshop site for a friend of mine who does textile printing. I have no idea how many users it will have, but I figured I'd be better safe than sorry. And of course, I expect to learn something from this project. Nik wrote:
Tari wrote:
If you don't want to pass in an expected user ID, checking that a new ID doesn't collide with an existing one before using it is fine but requires more database queries.
I am not sure I got this - what do you mean by passing an expected ID?As in include something in what use you to identify a user (probably a cookie) that has a user ID in addition to a session key, so you can check that the expected (provided) user ID matches a session associated with the same user.
So, if I append a timestamp and the username to the session ID, I protect against duplicates across users and across time, right? Is that a safe method?
I've come to the point where I need to protect users against brute force attacks. Is it a good idea to add a column "attempts" to my account table, which holds the number of failed attempts, and a column "time", which holds a delay time until the next allowed log in attempt?
The time is calculated as follows:
max(waitTimestamp - currentTimestamp, 0) + 2 ^ attempts + currentTimestamp
This selects either the time left to wait, or zero if no wait time is required. Then it adds a number exponentially depending on the number of attempts, to ensure a fast growing wait time. And finally it adds the current timestamp to create the next attempt's time.
If the attempts reach a certain number, I may lock the account down, issue an IP ban and send the attacked user an E-Mail notification with unlock instructions.
What I am trying to achieve here is a system that does not allow simply DoSing the user by adding long delays, does not allow easily annoying users by resetting their password, but prevents brute force attacks by making the waiting time longer and longer and eventually locking the intruder out.
Does this work? Is there anything I should change?
The time is calculated as follows:
max(waitTimestamp - currentTimestamp, 0) + 2 ^ attempts + currentTimestamp
This selects either the time left to wait, or zero if no wait time is required. Then it adds a number exponentially depending on the number of attempts, to ensure a fast growing wait time. And finally it adds the current timestamp to create the next attempt's time.
If the attempts reach a certain number, I may lock the account down, issue an IP ban and send the attacked user an E-Mail notification with unlock instructions.
What I am trying to achieve here is a system that does not allow simply DoSing the user by adding long delays, does not allow easily annoying users by resetting their password, but prevents brute force attacks by making the waiting time longer and longer and eventually locking the intruder out.
Does this work? Is there anything I should change?
I think that is perfect. I went a little more broad with my approach. I don't even check what user the visitor signs in as but instead log their IP. Basically, I grab their IP and cross check it with the table that stores login attempts. If the IP does not exist I create a new line and add in the date and time they attempted to log in and set the attempt column to 1.
If the IP exists, I replace the date and time with the new date and time, increment the attempts by 1. Once the attempts is equal to my threshold, I error them out. Even though I tell the visitor the cool down period, I continue to update the date and time even if they continue trying to login - such to prevent brute force after the 10 min period, if they set a script to try to log in and they continue to try to login while in the cool down, the cool down resets at every attempt. So, if you wait 9 minutes to try again, you'll be bumped back to 10 minutes whether the username and/or password were correct or not.
I also don't have a cron job to delete old entries. It's bad, I know, but it's a very uncommonly used portal that is just for myself. So, if a returning IP comes back their IP will be in the table but since the last time they tried could have been many days ago, their entry is just replaced with a new Date & Time an the attempt counter reset to 1.
I don't think my script for this has any sensitive info so if you'd like to me to share it, I can.
I definitely like the idea of sending the user an e-mail if someone tried to brute force their way in. Also, don't rely what was wrong. If you say "The password didn't match." or "Can't find the username" the attacker will know when they have a valid account. Just say something like "Invalid credentials" or "The username or password is incorrect." Something ambiguous that won't give the attacker more information; they could have a list of e-mails or usernames that they bought or scraped from somewhere and could put that list against your site, looking for valid usernames/e-mails and use those results to phish your users credentials.
If the IP exists, I replace the date and time with the new date and time, increment the attempts by 1. Once the attempts is equal to my threshold, I error them out. Even though I tell the visitor the cool down period, I continue to update the date and time even if they continue trying to login - such to prevent brute force after the 10 min period, if they set a script to try to log in and they continue to try to login while in the cool down, the cool down resets at every attempt. So, if you wait 9 minutes to try again, you'll be bumped back to 10 minutes whether the username and/or password were correct or not.
I also don't have a cron job to delete old entries. It's bad, I know, but it's a very uncommonly used portal that is just for myself. So, if a returning IP comes back their IP will be in the table but since the last time they tried could have been many days ago, their entry is just replaced with a new Date & Time an the attempt counter reset to 1.
I don't think my script for this has any sensitive info so if you'd like to me to share it, I can.
I definitely like the idea of sending the user an e-mail if someone tried to brute force their way in. Also, don't rely what was wrong. If you say "The password didn't match." or "Can't find the username" the attacker will know when they have a valid account. Just say something like "Invalid credentials" or "The username or password is incorrect." Something ambiguous that won't give the attacker more information; they could have a list of e-mails or usernames that they bought or scraped from somewhere and could put that list against your site, looking for valid usernames/e-mails and use those results to phish your users credentials.
So, I've been working on this thing for the past time, but, naturally, being my first time setting up a real website, I've run into some technical problems. I was setting up a daily cronjob to clean up the sessions table and delete the expired sessions. But it did not do anything. When I checked the error log, it reported the following, for each cronjob call:
Code:
It may be relevant that I set the cronjob file's permissions to 700, to prevent being called by random users. The cronjob itself was set up with a GUI from my shared hosting place, so I am unsure what exactly it did. I do have the options to specify a username and password for the cronjob, but I don't know what they do.
Also, note that I suspect that might be the fault, but don't have a .htaccess file in that folder. I don't know what I would put in there, really.
Code:
(13)Permission denied: [client adress stuff]: /path/.htaccess pcfg_openfile: unable to check htaccess file, ensure it is readable and that '/path/' is executable
It may be relevant that I set the cronjob file's permissions to 700, to prevent being called by random users. The cronjob itself was set up with a GUI from my shared hosting place, so I am unsure what exactly it did. I do have the options to specify a username and password for the cronjob, but I don't know what they do.
Also, note that I suspect that might be the fault, but don't have a .htaccess file in that folder. I don't know what I would put in there, really.
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.
» Go to Registration page
» Go to Registration page
Page 1 of 1
» All times are UTC - 5 Hours
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
Advertisement