PHP - Implementing Secure Login with PHP, JavaScript, and Sessions (without SSL)
HTTPS (HTTP over SSL) is the most common mechanism on the internet used to ensure server authenticity and provide data privacy. Unfortunately, SSL is often too complex and prohibitively expensive for many small-scale sites where all that is needed is a secure authentication mechanism.
Challenge-Response Authentication Mechanism
The biggest drawback of a regular non-SSL login is that the password is sent in clear-text, which can be easily sniffed by a potential attacker. But if the password were never to leave the client, there would be no chance of capturing it.
We can use a cryptographically-secure one-way hash function, such as MD5, to convert the password into a 128-bit hash number, which we could send instead. Even though this method would preserve the secrecy of the password, it would fail under a replay attack where an attacker could log in using the sniffed hash.
Fortunately, this attack can be easily averted through a shared random secret. First, the server generates a random challenge string which it sends to the client. The client then computes the hash digest of the challenge combined with the password to come up with the response. This response, along with the username, is sent to the server, which can then verify it because it can compute the same response using the original challenge and the looked-up password. The client is authenticated if this verification succeeds. The same challenge is never used again.
Client-side digest computation is made possible by Paj's MD5 JavaScript library.
Limitations
This method of authentication has certain limitations.
* Only the user password is secured. All other communication is sent in the clear. For example the username is sent in clear-text, which exposes the user's activity.
* Client may not support JavaScript, which is required to compute the MD5 response. ///Support for such clients may be added through the plain-text login.
* This mechanism only protects against passive eavesdropping. If an attacker was able to act as a proxy between the client and the server, it would be possible to substitute the login JavaScript mechanism with one that sent the password in clear. The attacker would then compute the response using the user's password and authenticate with the server.
Despite these limitations, the MD5 challenge-response method of authentication is in use by some of the most popular sites on the internet including Yahoo!, which is a testament of its credibility.
login.php
/////////////////////////////////////////////////////////////////////////////
//
// LOGIN PAGE
//
// Server-side:
// 1. Start a session
// 2. Clear the session
// 3. Generate a random challenge string
// 4. Save the challenge string in the session
// 5. Expose the challenge string to the page via a hidden input field
//
// Client-side:
// 1. When the completes the form and clicks on Login button
// 2. Validate the form (i.e. verify that all the fields have been filled out)
// 3. Set the hidden response field to HEX(MD5(server-generated-challenge + user-supplied-password))
// 4. Submit the form
//////////////////////////////////////////////////////////////////////////////////
session_start();
session_unset();
srand();
$challenge = "";
for ($i = 0; $i < 80; $i++) {
$challenge .= dechex(rand(0, 15));
}
$_SESSION[challenge] = $challenge;
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Login</title>
<script type="text/javascript" src="http://pajhome.org.uk/crypt/md5/md5.js"></script>
<script type="text/javascript">
function login() {
var loginForm = document.getElementById("loginForm");
if (loginForm.username.value == "") {
alert("Please enter your user name.");
return false;
}
if (loginForm.password.value == "") {
alert("Please enter your password.");
return false;
}
var submitForm = document.getElementById("submitForm");
submitForm.username.value = loginForm.username.value;
submitForm.response.value =
hex_md5(loginForm.challenge.value+loginForm.password.value);
submitForm.submit();
}
</script>
</head>
<body>
<h1>Please Login</h1>
<form id="loginForm" action="#" method="post">
<table>
<?php if (isset($_REQUEST[error])) { ?>
<tr>
<td>Error</td>
<td style="color: red;"><?php echo $_REQUEST[error]; ?></td>
</tr>
<?php } ?>
<tr>
<td>User Name:</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td> </td>
<td>
<input type="hidden" name="challenge" value="<?php echo $challenge; ?>"/>
<input type="button" name="submit" value="Login" onclick="login();"/>
</td>
</tr>
</table>
</form>
<form id="submitForm" action="authenticate.php" method="post">
<div>
<input type="hidden" name="username"/>
<input type="hidden" name="response"/>
</div>
</form>
</body>
</html>
authenticate.php
/////////////////////////////////////////////////////////////////////////////
//
// AUTHENTICATE PAGE
//
// Server-side:
// 1. Get the challenge from the user session
// 2. Get the password for the supplied user (local lookup)
// 3. Compute expected_response = MD5(challenge+password)
// 4. If expected_response == supplied response:
// 4.1. Mark session as authenticated and forward to secret.php
// 4.2. Otherwise, authentication failed. Go back to login.php
//////////////////////////////////////////////////////////////////////////////////
$userDB = array("john" => "abc123",
"bob" => "secret",
"anna" => "passwd");
function getPasswordForUser($username) {
// get password from a simple associative array
// but this could be easily rewritten to fetch user info from a real DB
global $userDB; return $userDB[$username];
}
function validate($challenge, $response, $password) {
return md5($challenge . $password) == $response;
}
function authenticate() {
if (isset($_SESSION[challenge]) &&
isset($_REQUEST[username]) &&
isset($_REQUEST[response])) {
$password = getPasswordForUser($_REQUEST[username]);
if (validate($_SESSION[challenge], $_REQUEST[response], $password)) {
$_SESSION[authenticated] = "yes";
$_SESSION[username] = $_REQUEST[username];;
unset($_SESSION[challenge]);
} else {
header("Location:login.php?error=".urlencode("Failed authentication"));
exit;
}
} else {
header("Location:login.php?error=".urlencode("Session expired"));
exit;
}
}
session_start();
authenticate();
header("Location:secret.php");
exit();
?>
common.php
////////////////////////////////////////////////////////////////////////////////
//
// COMMON PAGE
//
// Defines require_authentication() function:
// If the user is not authenticated, forward to the login page
//
////////////////////////////////////////////////////////////////////////////////
session_start();
function is_authenticated() {
return isset($_SESSION[authenticated]) &&
$_SESSION[authenticated] == "yes";
}
function require_authentication() {
if (!is_authenticated()) {
header("Location:login.php?error=".urlencode("Not authenticated"));
exit;
}
}
?>
secret.php
////////////////////////////////////////////////////////////////////////////////
//
// SECRET PAGE
//
// Invokes require_authentication() to ensure that the user is authenticated
//
////////////////////////////////////////////////////////////////////////////////
require("common.php");
require_authentication();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Secret Page</title>
</head>
<body>
<h1>This is a Secret Page</h1>
<p>You must have successfully authenticated since you are seeing this page.</p>
<p>
<a href="<?php echo $_SERVER[PHP_SELF]; ?>">View again?</a>
</p>
<p>
<a href="login.php">Logout?</a>
</p>
</body>
</html>
Related Marakana Courses
- PHP and MySQL Bootcamp Training
February 24th, 2007 at 4:38 am
Hi Zoran,
Nice article, and you described the basic process well.
Unfortunately I don't see a login form anywhere, and isn't using $_REQUEST considered bad form? Also, what you if want to protect all the files in a given directory (instead of just secret.php)?
It would be great if you could expand on this with an example.
Oh, here's the link for Paj's MD5 JavaScript library (which I'm looking at right after this
http://pajhome.org.uk/crypt/md5/
Thanks again,
John
March 4th, 2007 at 12:01 pm
I have also tried to get this work but to no avail. Can you provide an example pages, or include the required HTML and form info in your code? It would be a helpful starting point.
Thanks, great blog!
April 14th, 2007 at 3:47 pm
My bad! Somehow the original login form did not make it to this blog post. It's there now.
Thank you for bringing this to our attention!
August 30th, 2007 at 6:36 am
Hey man,
this is very nice BUT… You should not have passwords in database in plain-text!
September 16th, 2007 at 8:46 am
Is there something similar to this for user registration? My prob is that the php will never know what the password is..unless it has something to compare it to
September 24th, 2007 at 3:26 pm
Hellou!
What if, the database would have a hash number and the actual challenges answer will be calculated from the hash number (MD5 example) challenge. Then:
- database dosen't need to know the password.
- data what is sended changes every time
- hash can be calculated from the actual password
There is still at least one problems left. How to register the user safely
September 24th, 2007 at 3:27 pm
*answer will be calculated from the hash number (MD5 example) plus challenge…
The system didn't like the plus symbol…
November 25th, 2007 at 12:19 pm
I'm not clear on how storing the challenge in the user's session will prevent replay attacks, since both the challenge and the hashed password can be sniffed. Shouldn't the server globally store the challenges it has issued and only accept a given challenge string once?
November 25th, 2007 at 12:53 pm
Never mind my previous comment, didn't read your javascript method closely enough. I see you are not sending the challenge to the server when the form is submitted.
December 3rd, 2007 at 4:59 am
Like Martin said "You should not have passwords in database in plain-text!" so how would I allow this script to compare to a hashed password stored in a database?
January 8th, 2008 at 7:09 am
This script and form seem to be what I'm looking for but I'm unfamiliar with the way to set up the database required for this and how to communicate to the database. Can anyone direct me to a tutorial for that part? Also can someone tell me where to find a registration form similar to this one?
Thanks
January 18th, 2008 at 9:20 pm
There are these two little problems i found.
1 - Check line 12 in common.php there's a lil error at the end of it :
& &
should probably be replaced with a semi-colon
2- If i would go directly in secret.php [through changin the URL] i would get this error :
Warning: Cannot modify header information - headers already sent by (output started at /var/www/blog/common.php:23) in /var/www/blog/common.php on line 17
I thought that replacing :
header("Location:login.php?error=".urlencode("Not authenticated"));
which is in common.php with
echo "window.location='login.php';";
which uses javascript, does the work. and sends me automatically to login.php. That's all.
Thanks a lot sasa
March 6th, 2008 at 6:28 am
NICE PROGRAM….. I SUCCESSFULLY RUN THE PROGRAM….
1 - Check line 12 in common.php there's a liTTLE error at the end of it :
& &
should probably be replaced with a semi-colon.
March 9th, 2008 at 11:46 am
does any one know how to execute remember user name and password in the browser
April 4th, 2008 at 9:09 am
Thanks for sharing this. Seems like the best way without going ssl.
April 6th, 2008 at 5:41 am
im building a webstie at the min and have used this function which works great but just wondered as i want to use this on several pages so if the a user logs in they can access several pages but i want to obviously block them if they try and acces them any toher way . this script only blocks the secret.php page but if wanted to add more loctions than this how would i do it ?
can anyone please help
thank you
April 10th, 2008 at 12:22 pm
I just wasted about an hour trying to figure out why nothing was working.
A lot of people type their username, press [Tab], type their password then press [Enter].
The "onclick=login()" was never triggered… You should change that to something like "onsubmit" onto the form itself.
June 12th, 2008 at 5:03 pm
I would just like to say, this is a fantastic script. I was using it without any issues until the fact that my client requested the passwords in the database to be hashed. I created an md5 hash for passwords now, and for some reason, I just cannot get the damn thing to work.
Does anyone have an idea in a way where the script executes a function which re-generates the hash for the password, and checks to see whether it equals the same or not?
If I can't find a way around this little speedhump, I will have to unfortunately refer to other methods (or just create my own with what little knowledge I possess).
The function I have created and would like to call goes a little something like this:
starts function, reads a variable which gets called when the function runs, starts an md5 hash, and then creates another variable where the variable called earlier is encrypted.
I need a way of calling this function, somehow encrypting the submitted password variable in the script using the md5 hashing process, and then read it against the database hash variable.
Has anyone an idea or am I unfortunate enough to have to give up such a great script?
June 30th, 2008 at 2:42 pm
Mark,
I have modified the above example scripts to make a system as suggested by Sebastian. The server sends the registration form with a random number (I used a 128-bit hex number), which becomes the new user's "salt". Embedded Javascript performs an HMAC-MD5 hash with the password and the salt and returns the username and the salted hashed password. The server now has the username, the user's salt, and the salted-hashed password. (Ideally this registration would take place over a secure connection).
The login is then a two-stage process: (1) The user sends the username, and the server returns the user's "salt" and a random challenge string. (2) The user enters his/her password on the login form, and embedded Javascript re-creates the salted & hashed password (the same one the server knows) and hashes THAT with the random challenge string, which it sends to the server. Since the server knows both the challenge string it sent and also the salted & hashed password, it can verify that the user has entered the correct password without knowing what that password is.
The server should keep track of which challenge strings have been used and not re-use them. And of course, if an attacker gets into the database, he can hack together a login form using the hashed password to gain access to your system. But at least he won't know the password, which is handy since a lot of users use the same password for multiple systems.
Some other articles I found helpful:
http://www.developerfusion.co.uk/show/4679/4/
http://pajhome.org.uk/crypt/md5/auth.html
July 9th, 2008 at 7:54 pm
Nice login without SS, and absolutely invulnerable of man in middle attacks… thanks
November 6th, 2008 at 10:42 am
I cant find how it connecnts to DB
can u tell me where exactly it is? to connect to my db
and if you guys know any source where can find code which will help me to make the following….
i whant to make users which can change the content of div element on the page…
something like this comment box..
but i whant to give this permission manualy to users and also activate there accounts manualy (like on forums)
and secured as much as it posible
i am noob in php, if u guys can give me any source
p.s sorry for english =/
November 23rd, 2008 at 8:01 pm
[...] Referenced article: PHP - Implementing Secure Login with PHP, JavaScript, and Sessions (without SSL) [...]
December 28th, 2008 at 6:57 pm
[...] "Secure login without SSL" [Marakana] Written by evan | [...]
March 2nd, 2009 at 2:28 pm
Many thanks, perfect job.
April 28th, 2009 at 5:25 am
[...] See original here: PHP - Implementing Secure Login with PHP, JavaScript, and Sessions … [...]
May 2nd, 2009 at 10:42 pm
Seems to be working well for me. Im trying to use the code to make all my pages non accessible unless already logged in. not sure if i can do it with this code. Thanks tho!
May 22nd, 2009 at 4:32 am
Nice implementation - was thinking of doing client side hashing in my own forms.
On a side note - there's no need to call srand() - php 4.2+ calls it automatically.
May 27th, 2009 at 12:24 pm
Thanks so much for this example code!
Commenter #13 noted a bug in common.php:
return isset($_SESSION[authenticated]) &&
$_SESSION[authenticated] == "yes";
This is probably due to some automated conversion of the AND operator && to HTML. The code should read as one line:
return isset($_SESSION[authenticated]) && $_SESSION[authenticated] == "yes";
Here's hoping the Spark blog engine's comment cleaner doesn't mangle my comment ;p
May 29th, 2009 at 7:09 am
hey i found a bug in your code. i see that it has been a while so i dont know if you have fixed it yet.but :
in firefox, install http live headers and firebug.
firebug will allow you to view the html on the web page. as well as hidden fields.when looking at the hidden fields, you can get the current challenge string and use that for a replay attack.
firebug and live http headers where the tools i used, there could be others. iam thinking of a solution to this. if i find one ill let you know. but if anyone has a solution for this please let me know.
one idea i have is have a separate php class that handles all the challenge strings but i am not too happy on the idea.it just feels like a ducktape and bubble gum solution.
August 17th, 2009 at 3:46 am
For those who still wonder how to make the form working with ENTER and CLICK, just change the <input type="button" as <input type="submit" and remove the onclick from it
should look like this at the end
Error
User Name:
Password:
<input type="hidden" name="challenge" value=""/>
August 17th, 2009 at 3:47 am
it didnt like the hole paste, so just this
instead of this
August 17th, 2009 at 3:47 am
shitty website
August 17th, 2009 at 4:28 pm
На таких громких заголовках и подобной шумихе можно делать и не такие успехи
September 1st, 2009 at 12:01 am
It is a piece of great news to know about GA! It occupies a large proportion of my earning.
September 28th, 2009 at 10:45 am
Great script..!!
Is there some way it can be adjusted to function on different homepages placed on the same server without remembering the session. I have placed this script on some different homepages and when I log into homepage1 I can go directly to the secret site on homepage two without signing in even though usernames and passwords are dont match.
December 3rd, 2009 at 3:46 am
Hi Tried using this script for my website. But some how $REQUEST[username] always takes username as tomcat, No matter what I give in username field. Any guess what is going wrong ?
December 25th, 2009 at 1:04 am
Hi!
There is a major security problem in this aproach.
You mention, that you do not need TLS aka SSL for this method. Your JavaScript would prevent MITM-Attacks.
But the truth is, if someone is able to perform a MITM then he can also change the content sent to the browser. There are two methods in my mind for attacking this:
1) Send a different md5 javascript function. This could perform as the original, but also generates a hidden field in the form with the original password and username. So the information is sent in clear text for the attacker and hashed for your script.
2) Change the action="" attribute of your form and disable the javascript (onclick, onsubmit, whatever). So the username and pass the user typed in the form will be sent to my page.
Sorry, but this script will not prevent any type of MITM.
January 12th, 2010 at 1:51 am
If we're considering that the users traffic is being sniffed, why can't the attacker just capture the incoming salt, or salted hashed password + username and login with their own script?
March 20th, 2010 at 11:33 am
It seems this solution considers passwords are stored in plain text in the DB, which is not good.
Admitting we store md5 of the passwords in DB, the Js should be changed (notice the md5 on the password value) :
submitForm.response.value =
hex_md5(loginForm.challenge.value+hex_md5(loginForm.password.value));
May 6th, 2010 at 9:09 am
You'd be better to use SHA1 instead of MD5.
June 11th, 2010 at 5:57 am
Thanks for the code. a couple mods I made:
1) Set cookie with session code, username, and ip address to continually check against
2) store a "fingerprint" on MySQL which is an md5 hash of user agent and ip to compare against
@tienod, I needed a fairly secure solution for a site that wouldn't be viewed by anyone outside of a couple of users, so I set up a localhost page that downloaded the login page and the md5.js file, and compared it against expected content. If that matches, then I use cURL to submit the user and password. Thanks for the heads up on the possible vulnerability.
June 22nd, 2010 at 12:22 am
This is nice script and easy to understand..
Its good, thanks forever:-)
August 22nd, 2010 at 12:31 pm
Excellent tutorial. I've realized that my site, which lacks SSL, has a security vulnerability for some time but have been reluctant to install it because of the complexity and cost involved. You spelled out a solution (at least for the time being) in clear and helpful terms.