HTTP authentication is quite popular for web applications. It is pretty
easy to implement and works for a range of http applications; not to mention
your browser.
Basic Auth
The two main authentication schemes are 'basic' and 'digest'. Basic is pretty
easy to implement and appears to be the most common:
- <?php
- $username = null;
- $password = null;
- // mod_php
- if (isset($_SERVER['PHP_AUTH_USER'])) {
- $username = $_SERVER['PHP_AUTH_USER'];
- $password = $_SERVER['PHP_AUTH_PW'];
- // most other servers
- } elseif (isset($_SERVER['HTTP_AUTHENTICATION'])) {
- if (strpos(strtolower($_SERVER['HTTP_AUTHENTICATION']),'basic')===0)
- list($username,$password) = explode(':',base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
- }
- if (is_null($username)) {
- header('WWW-Authenticate: Basic realm="My Realm"');
- header('HTTP/1.0 401 Unauthorized');
- echo 'Text to send if user hits Cancel button';
- die();
- } else {
- echo "<p>Hello {$username}.</p>";
- echo "<p>You entered {$password} as your password.</p>";
- }
- ?>
Well it's a bit difficult I suppose, but you might have noticed the username
and password are sent over the wire using base64 encoding. Not really secure, unless
you have SSL in place.
Digest
Digest is designed to be more secure. The password is never sent over the wire
in plain text, but rather as a hash. The implications of the usage of a hash is that
it can never be decrypted. We can only validate the hash by applying the same hash function
to the password we have. If the hashes match, the password was correct.
Lets first see how Digest auth should work:
Client requests url
- GET / HTTP/1.1
Server requires authentication
- HTTP/1.1 401 Unauthorized
- WWW-Authenticate: Digest realm="The batcave",
- qop="auth",
- nonce="4993927ba6279",
- opaque="d8ea7aa61a1693024c4cc3a516f49b3c"
Client authenticates
- GET / HTTP/1.1
- Authorization: Digest username="admin",
- realm="The batcave",
- nonce=49938e61ccaa4,
- uri="/",
- response="98ccab4542f284c00a79b5957baaff23",
- opaque="d8ea7aa61a1693024c4cc3a516f49b3c",
- qop=auth, nc=00000001,
- cnonce="8d1b34edb475994b"
Information coming from the server:
| realm | A string which will be used within the UI and as part of the hash. |
| qop | Can be auth and auth-int and has influence on how the hash is created. We use auth. |
| nonce | A unique code, which will be used within the hash and needs to be sent back by the client. |
| opaque | This can be treated as a session id. If this changes the browser will deauthenticate the user. |
Information from the client:
| username | The supplied username |
| realm | Same as server response. |
| nonce | Same as server response. |
| uri | The authentication uri |
| response | The validation hash. |
| opaque | Same as server response. |
| qop | Same as server response. |
| nc | Nonce-count. This a hexadecimal serial number for the request. The client should increase this number by one for every request. |
| cnonce | A unique id generated by the client |
So how do we know if the password was correct? We van validate using the following formula (pseudo code).
- A1 = md5(username:realm:password)
- A2 = md5(request-method:uri) // request method = GET, POST, etc.
- Hash = md5(A1:nonce:nc:cnonce:qop:A2)
- if (Hash == response)
- //success!
- else
- //failure!
Or, using PHP:
- <?php
- $realm = 'The batcave';
- // Just a random id
- $nonce = uniqid();
- // Get the digest from the http header
- $digest = getDigest();
- // If there was no digest, show login
- if (is_null($digest)) requireLogin($realm,$nonce);
- $digestParts = digestParse($digest);
- $validUser = 'admin';
- $validPass = '1234';
- // Based on all the info we gathered we can figure out what the response should be
- $A1 = md5("{$validUser}:{$realm}:{$validPass}");
- $A2 = md5("{$_SERVER['REQUEST_METHOD']}:{$digestParts['uri']}");
- $validResponse = md5("{$A1}:{$digestParts['nonce']}:{$digestParts['nc']}:{$digestParts['cnonce']}:{$digestParts['qop']}:{$A2}");
- if ($digestParts['response']!=$validResponse) requireLogin($realm,$nonce);
- // We're in!
- echo 'Well done sir, you made it all the way through the login!';
- // This function returns the digest string
- function getDigest() {
- // mod_php
- if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
- $digest = $_SERVER['PHP_AUTH_DIGEST'];
- // most other servers
- } elseif (isset($_SERVER['HTTP_AUTHENTICATION'])) {
- if (strpos(strtolower($_SERVER['HTTP_AUTHENTICATION']),'digest')===0)
- $digest = substr($_SERVER['HTTP_AUTHORIZATION'], 7);
- }
- return $digest;
- }
- // This function forces a login prompt
- function requireLogin($realm,$nonce) {
- header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",nonce="' . $nonce . '",opaque="' . md5($realm) . '"');
- header('HTTP/1.0 401 Unauthorized');
- echo 'Text to send if user hits Cancel button';
- die();
- }
- // This function extracts the separate values from the digest string
- function digestParse($digest) {
- // protect against missing data
- $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
- $data = array();
- preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);
- foreach ($matches as $m) {
- $data[$m[1]] = $m[2] ? $m[2] : $m[3];
- unset($needed_parts[$m[1]]);
- }
- return $needed_parts ? false : $data;
- }
- ?>
As you can see we need to have a plain-text version of the password in order to
validate the user. It's not a good idea to store the plain-text password, therefore
it's strongly recommended to store the result of $A1 instead.
Security improvements
- It's smart to validate the contents of opaque, nonce and realm. If you have
the data stored on the server, why not check it. - The nc should be an ever increasing number. You could store the number and
track to make sure it doesn't make any big jumps. It's not wanted to be extremely
strict about the sequence, because you might miss a number, and requests could come in
be out of order. - 'qop' is quality of protection. This serves as an integrity code for the request.
A hacker could steal all your HTTP Digest headers and simply change the body to make it do
something else. If 'qop' is set to 'auth', only the requested uri will be taken
into consideration. If 'qop' is 'auth-int' the body of the request will also be used in the hash. (A2 = md5(request-method:uri:md5(request-body))).

The PEAR package Auth_SASL I wrote may be of some interest, implementing a variety of SASL authentication methods: http://pear.php.net/package/Auth_SASL
the problem with HTTP auth is how do you logout ?
Your digest authentication is open to a replay attack, you can do a few things to solve this.
1. Keep track of any previous nonce values and make sure they're not re-used.
2. Attach a timestamp to the nonce and if a timestamp it outwith a defined time frame, reject it.
3. Add in the IP address as part of the nonce and check it on the next request.
I recommend a combination of at least two though.
Richard, I'll definitely take a look. Especially in relation to some of the points Scott just mentioned :)
Sak, the trick is to just trigger another HTTP 401. If you do this, the user will be forced to reauthenticate.
Scott,
1 & 2 make sense, both definitely a bit tedious to implement. Curious to how other people do this..
I am curious too about how to logout.
Any ideas on this? I don't want to just log in as one user.
Is it possible to set a bad nonce on log out request?
As mentioned before, all you really need to do is trigger a 401.
As a starting point you could use:
http://code.google.com/p/sabredav/source/browse/trunk/lib/Sabre/HTTP/
And call ->requireLogin();
About logout.
I can throw 401 but trouble happens when user press "cancel" on authentication form and then browser's "back". In this case browser does not purge authentication cache and user still logged in.
Hi Evert. Nice help! First time here, and obviously found ur article via google...
Wanna suggest a little correction:
"(...)
$A1 = md5("{$digestParts['username']}:{$realm}(...)"
$digestParts['username'] should be replaced by $validUser, don't???
else any username'll be validated by the correct pwd. Changed it here and sucessfully works.
a simple workaround to do logout function is merge http-auth with php session... just start a session at line 1 and create an if to check a logout function to close the section....
sorry 4 bad english... and thank you again...
Evandro, that's a very good point. I'm fixing this :)
Note that in real-life, you would actually check the username coming in from the request, then fetch the password information.
So the only reason why this would be somewhat of a security issue, is if you really only just have 1 hardcoded username + pass.
...ably know that HTTP also has native authentication, in the form of Basic and Digest authentication (read my older article if you want to know how). Every browser supports it, and as a bonus pretty much every other HTTP cl...