A while ago, (okay, a long while ago) I wrote about a way to improve the security of login/authentication with web applications. The process involved using challenge-response during authentication to prevent passwords from being transmitted in plaintext. The idea was not mine, but instead the work of a smart fellow named Paul Johnston. At the time, I “hoped to present an actual implementation” sometime in the future, but never got around to it. I finally had some time and decided to put together a working example using PHP and JavaScript.
Download the source
Please feel free to download and try out the first public release of the CHAP-PHP login system. The zip file has the full source and provides an example how to implement the system both on the client and server side.
An online demo of the system is also available.
Improving authentication security with Challenge-Response
Challenge-Response is the basis for many authentication systems. In such a situation, a server may have to authenticate a user by verifying their credentials, usually in the form of a password. However, transmitting plaintext passwords over connections that are not secure can lead to compromises. In such a situation, challenge-response may be used. This usually involves the server sending a random challenge string to the client, which must then produce an appropriate response that can only be computed using the challenge and the password. This response is then sent to the server, which can then verify if the right password was used to generate it. The response is usually computed by hashing a value that depends on the challenge and the password, thus it is not possible to obtain the password from the response, which might have been sniffed on an insecure connection.
However, such a traditional challenge-response has the downside that the plaintext password (or a password-equivalent) must be known to the server at some point. Paul Johnston came up with an idea for an alternative system a while ago that overcomes these shortcomings. (Though it is not free from weaknesses itself) It is this “Alternative System” that the above release is based upon. Here is a quick explanation of the system, adapted from Johnston’s site:
Signup:
1. Server sends random1
2. Client sends hex_sha1(hex_hmac_sha1(password, random1))Login:
1. Server sends random1 and random2
2. Client sends hex_hmac_sha1(password, random1) and hex_sha1(hex_hmac_sha1(password, random2))
During registration, the value sent by the client is stored. During login, the user must present a value that when hashed produces the value provided at registration. Because of the non-reversibility property of hashes, knowing the value passed during registration does not allow an attacker to login. The only way to produce the valid response is to know the actual plaintext password, which is never transmitted or known by the server. In this system, challenges are linked to a user and must be stored, since it must be known what challenge was used to produce the response.
The second random challenge (random2) and the second value sent by the client during login are used to prevent replay attacks. Upon successful login, the second value provided by the client becomes the next response, equivalent to the value first provided during registration. Thus, for the next login, the value that is sent must hash to equal this value. This also means that the challenges are updated/changed each time a login is successful. This has the unfortunate downside of revealing when a user has logged in, since the challenge presented at login will be different. (Challenges must be publicly available)
For a more thorough explanation, I suggest that you read Johnston’s article on the subject.
Getting it to work
Understanding the process above leads to the conclusion that user-login is now a two-stage process. Since the challenges are tied to a user, the username must first be known to the server in order to retrieve the challenge for that user. The client can then use the challenge to produce the response to send to the server for authentication.
Having a two-stage login form would be very unfriendly to users. Thus, the main challenge is to make it appear as if nothing out of the ordinary is happening. This is where Ajax comes into play. When the form is submitted, the event is prevented from occurring normally. Instead, the username is first retrieved from the form and sent to the server via an Ajax call in order to retrieve the associated challenge. Once the challenge is received, the appropriate responses are computed, inserted into the form and then the form is submitted.
The current system also works in the case that JavaScript is not enabled/available on the client side. In this case, challenge-response will not be available, since JavaScript is used to compute the responses. The server-side PHP scripts infer that JavaScript was not enabled on the client-side if proper challenge-responses are not received, and thus treat the password as plaintext. In this case, passwords are transmitted in plaintext. With this code, you have the choice of allowing this “insecure” login to proceed or not.
Note that the CHAP-PHP is more of a module than a full-fledged system, since it’s not intended to be used on its own but instead as part of some application. It might be a bit confusing if you’re a non-developer, but I’ve tried to make it as straightforward and simple as possible so that it will be easy to integrate with existing code bases/frameworks/sites.
Please don’t hesitate to contact me with your questions, comments or suggestions.
Disclaimer and warning
You should not use this as the basis for authentication for sensitive data/websites. I am not a security expert. At this point, this is more of a proof-of-concept then something concrete. It is intended to be the starting point for perhaps something more secure and to show that there are alternatives for more secure authentication when SSL is not available.
Revision History
- 0.5 – First Public Release – 2008-03-29
- 0.5.1 – Fixed file-based storage to be more robust – 2008-03-30
Thanks, I’ll try it out and try to comment on it later on this week!
Peter,
thanks for your thoughts and this script. It’s worth to have a deeper look inside.
One improvement I have found by now: you can check if the password is equal to the username on the server-side too.
For this you could insert the following lines of code into ChapAuthenticationImpl.php below the block for checking if the pwd is empty (line 205):
// Check if the plaintext password is the same as the username.
else if ($this->passwordPlainTextTransform($username, $challenge1) == $password)
{
throw new ChapAuthenticationException('Password cannot be equal to the username');
}
@Jens
Thanks for the tip! I will incorporate your changes into the next version…