HTTP authentication with PHP

It is possible to use the
header() function to send an "Authentication Required"
message to the client browser causing it to pop up a Username/Password
input window. Once the user has filled in a username and a password,
the URL containing the PHP script will be called again with the
predefined variablesPHP_AUTH_USER, PHP_AUTH_PW,
and AUTH_TYPE set to the user name, password and
authentication type respectively. These predefined variables are found
in the $_SERVER and
$HTTP_SERVER_VARS arrays. Both "Basic" and "Digest"
(since PHP 5.1.0) authentication methods are supported. See the
header() function for more information.

Please be careful when coding the HTTP header lines. In order to guarantee maximum
compatibility with all clients, the keyword "Basic" should be written with an
uppercase "B", the realm string must be enclosed in double (not single) quotes,
and exactly one space should precede the 401 code in the
HTTP/1.0 401 header line. Authentication parameters have
to be comma-separated as seen in the digest example above.

Instead of simply printing out PHP_AUTH_USER
and PHP_AUTH_PW, as done in the above example,
you may want to check the username and password for validity.
Perhaps by sending a query to a database, or by looking up the
user in a dbm file.

Watch out for buggy Internet Explorer browsers out there. They
seem very picky about the order of the headers. Sending the
WWW-Authenticate header before the
HTTP/1.0 401 header seems to do the trick
for now.

As of PHP 4.3.0, in order to prevent someone from writing a script which
reveals the password for a page that was authenticated through a
traditional external mechanism, the PHP_AUTH variables will not be
set if external authentication is enabled for that particular
page and safe mode is enabled. Regardless,
REMOTE_USER can be used
to identify the externally-authenticated user. So, you can use
$_SERVER['REMOTE_USER'].

Note:
Configuration Note

PHP uses the presence of an AuthType directive
to determine whether external authentication is in effect.

Note, however, that the above does not prevent someone who
controls a non-authenticated URL from stealing passwords from
authenticated URLs on the same server.

Both Netscape Navigator and Internet Explorer will clear the local browser
window's authentication cache for the realm upon receiving a
server response of 401. This can effectively "log out" a user,
forcing them to re-enter their username and password. Some people
use this to "time out" logins, or provide a "log-out" button.

This behavior is not required by the HTTP Basic
authentication standard, so you should never depend on this. Testing with
Lynx has shown that Lynx does not clear
the authentication credentials with a 401 server response, so pressing back
and then forward again will open the resource as long as the credential
requirements haven't changed. The user can press the
'_' key to clear their authentication information, however.

Also note that until PHP 4.3.3, HTTP Authentication did not work
using Microsoft's IIS server with the CGI version of PHP due to a
limitation of IIS. In order to get it to work in PHP 4.3.3+,
you must edit your IIS configuration "Directory Security".
Click on "Edit" and only check
"Anonymous Access", all other fields
should be left unchecked.

Another limitation is if you're using the IIS module (ISAPI) and PHP 4, you
may not use the PHP_AUTH_* variables but instead, the
variable HTTP_AUTHORIZATION is available. For example,
consider the following code: list($user, $pw) = explode(':',
base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));

Note:
IIS Note:
For HTTP Authentication to work with IIS, the PHP directive
cgi.rfc2616_headers must
be set to 0 (the default value).

Note:

If safe mode is enabled, the
uid of the script is added to the realm part of
the WWW-Authenticate header.

Be careful using http digest authentication (see above, example 34.2) if you have to use the 'setlocale' function *before* validating response with the 'http_digest_parse' function, because there's a conflict with \w in the pattern of 'preg_match_all' function :

In fact, as \w is supposed to be any letter or digit or the underscore character, you must not forgot that this may vary depending on your locale configuration (eg. it accepts accented letters in french)...

Due to this different pattern interpretation by the 'preg_match_all' function, the 'http_digest_parse' function will always return a false result if you have modified your locale (I mean if your locale accepts some extended characters, see http://fr.php.net/manual/en/reference.pcre.pattern.syntax.php for further information).

IMHO, I suggest you not to use setlocale before having your authentication completed...

First, we decode the base64 encoded string discarding the first 6 characters of "Basic " and then we do a regular validation.At the end of the script we print the variables to verify it's working. This should be ommited in the production version.

It's a variation of the script by Bernard Paques.Thanks to him for that snippet.

Just tell browser that it is successfully logged in!This works as follows:1. User clicks logout button2. Script sends 401 Header3. User does NOT enter a password4. If no password is entered, script sends 200 Header

I came up with another approach to work around the problem of browsers caching WWW authentication credentials and creating logout problems. While most browsers have some kind of way to wipe this information, I prefer having my website to take care of the task instead of relying on the user's sanity.

Even with Lalit's method of creating a random realm name, it was still possible to get back into the protected area using the back button in Firefox, so that didn't work. Here's my solution:

Since browsers attach the credentials to specific URLs, use virtual paths where a component of the path is actually a PHP script, and everything following it is part of the URI, such as:

By choosing a different number for the last component of the URL, browsers can be tricked into thinking that they are dealing with a completely different website, and thus prompting the user for credentials again.

Note that using a random, unrestricted number will still allow the user to hit the back button to get back into the page. You should keep track of this number in a server-side file or database and regenerate it upon each successful login, so that the last number(s) become invalid. Using an invalid number might result in a 403 response or, depending on how you feel that day, a 302 to a nasty website.

Care should be taken when linking from the page generated in this case, since relative links will be relative to the virtual and non-existant directory rather than the true script directory.

A simple script for SSL Client Certificate authentication with a basic authentication fall-back. I use this on my site using LDAP server to check username/passwords and client certificate to user mapping.

<? // Check if and how we are authenticatedif ($_SERVER['SSL_CLIENT_VERIFY'] != "SUCCESS") { // Not using a client certificate if ((!$_SERVER['PHP_AUTH_USER']) && (!$_SERVER['PHP_AUTH_PW'])) { // Not logged in using basic authentication authenticate(); // Send basic authentication headers }}

I couldn't get authentication to work properly with any of the examples. Finally, I started from ZEND's tutorial example at:http://www.zend.com/zend/tut/authentication.php?article=authentication (validate using .htpasswd) and tried to deal with the additional cases. My general conclusion is that changing the realm is the only reliable way to cause the browser to ask again, and I like to thank the person who put that example in the manual, as it got me on the right path. No matter what, the browser refuses to discard the values that it already has in mind otherwise. The problem with changing the realm, of course, is that you don't want to do it within a given session, else it causes a new request for a password. So, here goes, hopefully the spacing isn't too messed up by the cut'n'paste.

I spent the better part of a day getting this to work right. I had a very hard time thinking through what the browser does when it encounters an authentication request: seems to me that it tries to get the password, then reloads the page... so the HTML doesn't get run. At least, this was the case with IE, I haven't tested it with anything else.

// Below here runs HTML-wise only if there isn't a $_SESSION, // and the browser *can't* set $PHP_AUTH_USER... normally // the browser, having gotten the auth info, runs the page // again without getting here. // What I'm basically getting to is that the way to get // here is to escape past the login screen. I tried // putting a session_destroy() here originally, but the // problem is that the PHP runs regardless, so the // REFRESH seems like the best way to deal with it.echo "<meta http-equiv=\"REFRESH\" content=\"0;url=index.php\">" ; exit; }

are both insecure, they could leave this code open to SQL injection, you should always remove invalid characters in both, or at least encode them.

Actually storing passwords as MD5 hashes leaves you less work to secure.

Second security risksThe same mysql user has rights to both update and select, and possibly even insert and on your auth database no less.Again the SQL inject attack may occur with this., and the end user could then change the users username, password, or anything else in relation to this.

Third items is more of a performance issue,

Do you really need to update the database, as updates are slower then selects, and if you do them every time they access the page, you are costing some speed penalty.

One option, if you want to use sql (I think mysql has it) is memory only databases, and create a table within memory, the stores a unique session identifier for each user, that is logged in, or alternatively if it's a single front end system, you could use db files.

To anybody who tried the digest example above and didn't get it to work.

For me the problem seemed to be the deprecated use of '\' (backslash) in the regex instead of the '$' (Dollar) to indicate a backreference. Also the results have to be trimmed off the remaining double and single quotes.

In the german example #2 (digest), the <?php $realm = "Gesch├╝tzter Bereich"; ?>. As far as I have tested the umlaut ├╝ is problematic, resulting in an password enter infinity loop. In my case it was written in UTF-8. So I suggest using only plain ASCII characters for the realm.

I tried example 7, and at first I couldn't get it to work. It took me a while to spot that somewhere along the line, probably by the server, a seemingly random number was being added to the realm - so the valid_result variable wasn't calculated using the correct realm.

To get around this, or any similar problems, make the following changes to the example:

Regarding HTTP authentication in IIS with the php cgi 4.3.4, there's one more step. I searched mightily and didn't find this information anywhere else, so here goes. When using HTTP auth with the php CGI, you need to do the following things:

3. In "Custom Errors", select the range of "401;1" through "401;5" and click the "Set to Default" button.

It's this last step that is crucial, yet not documented anywhere. If you don't, instead of the headers asking for credentials, IIS will return its own fancy but useless 'you are not authenticated' page. But if you do, then the browser will properly ask for credentials, and supply them in the $_SERVER['PHP_AUTH_*'] elements.

While Digest authentication is still far superior to Basic authentication, there are a number of security issues that one must keep in mind.

In this respect, the Digest example given above is somewhat flawed, because the nonce never times out or otherwise become invalid. It thus becomes a password-equivalent (although to that specific URL only) and can be used by an eavesdropper to fetch the page at any time in the future, thus allowing the attacker to always access the latest version of the page, or (much worse) repeatedly invoke a CGI script -- for instance, if the user requests the URL "/filemanager?delete=somefile", the attacker can repeat this deletion at any point in the future, possibly after the file has been recreated.

And while it might not be possible to change GET data without reauthentication, cookies and POST data *can* be changed.

To protect against the first problem, the nonce can be made to include a timestamp, and a check added to ensure that nonces older than e.g. 30 minutes result in a new authentication request.

To solve the second problem, a one-time only nonce needs to be generated -- that is, all further requests using a particular nonce must be refused.

One way to do this: When the user requests an action such as "deletefile", store a randomly generated nonce in a session variable, issue a 401 authentication challenge with that nonce, and then check against the stored value when receiving the authentication (and clear the session variable).

This way, although a possible eavesdropper receives the nonce and thus gains the ability to perform the action, he can only perform it once -- and the user was going to perform it anyway. (Only the user or the attacker, but not both, gets to perform the action, so it's safe.)

Of course, at some point, the security can only be improved by switching to HTTPS / SSL / TLS (this is for instance the only way to defend against man-in-the-middle attacks). You decide the level of security.

return $parts;}?>Give it a try at http://www.creativetheory.ca/ /tests/ta1.php Log in with user test password pass or user guest password guest. Go to page two for links to the code. Comments, ideas, suggestions, or critique welcome.

In case of CGI/FastCGI you would hot be able to access PHP_AUTH* info because CGI protocol does not declare such variables (that is why their names start from PHP) and server would not pass them to the interpreter. In CGI server should authenticate user itself and pass REMOTE_USER to CGI script after it.

So you need to "fetch" request headers and pass them to your script somehow.

In apache you can do it via environment variables if mod_env is installed.

Following construction in .htaccess copies request header "Authorization" to the env variable PHP_AUTH_DIGEST_RAW

SetEnvIfNoCase ^Authorization$ "(.+)" PHP_AUTH_DIGEST_RAW=$1

You can now access it via $_ENV.

Do not forget to strip auth type ("Digest" in my case) from your env variable because PHP_AUTH_DIGEST does not have it.

If mod_env is not installed you probably have mod_rewrite (everyone has it because of "human readable URLs").

---If you use ZF you probably use Zend_Auth_Adapter_Http to auth user.

It takes Authorization info using "Zend_Controller_Request::getHeader"This method uses apache_request_header which is likely not to be accessible in old CGI/FastCGI installations or _$_SERVER['HTTP_<HeaderName>] , so you need to put your authentication data, obtained via _GET or ENV to _$_SERVER['HTTP_AUTHORIZATION']. It will make ZF work transparently with you solution and I believe any other framework should work also

In Windows 2003 Server/IIS6 with the php4+ cgi I only get HTTP authentication working with:<?php header("Status: 401 Access Denied"); ?>with<?php header('HTTP/1.0 401 Unauthorized'); ?>doesn't work !I also need in "Custom Errors" to select the range of "401;1" through "401;5" and click the "Set to Default" button.Thanks rob at theblip dot com

Say you have password and groups files in standard Apache format (htpasswd etc.), but you want to apply authorization based on something other than filename, ie something you can't catch in .htaccess. You want to emulate the server behavior in PHP -- the equivalent of:

Don't use apache authentification in plain text. Is more better to use own script to generete new ID which is relevant to password. Apache auth data are sent to every page, so the posible mistake are known.

You shouldn't use the "last" ("L") directive in the RewriteRule! This will prevent all further rewrite rules to be skipped whenever a Basic or Digest Auth is given, which is almost certainly not what you want.

So the following lines are sufficient for the .htaccess (or httpd.conf) file:

First of all, sorry for my English.One more authorization script with logout solution.In script given by meint_at_meint_dot_net (http://www.php.net/manual/en/features.http-auth.php#93859) rewrite_module is used. If there are any problems with that module and possibilities of administration of the web-server are restricted (including restrictions for use of .htaccess) then you can use this solution.

I came up with another approach to work around the problem of browsers caching WWW authentication credentials and creating logout problems. While most browsers have some kind of way to wipe this information, I prefer having my website to take care of the task instead of relying on the user's sanity.

Even with Lalit's method of creating a random realm name, it was still possible to get back into the protected area using the back button in Firefox, so that didn't work. Here's my solution:

Since browsers attach the credentials to specific URLs, use virtual paths where a component of the path is actually a PHP script, and everything following it is part of the URI, such as:

By choosing a different number for the last component of the URL, browsers can be tricked into thinking that they are dealing with a completely different website, and thus prompting the user for credentials again.

Note that using a random, unrestricted number will still allow the user to hit the back button to get back into the page. You should keep track of this number in a server-side file or database and regenerate it upon each successful login, so that the last number(s) become invalid. Using an invalid number might result in a 403 response or, depending on how you feel that day, a 302 to a nasty website.

Care should be taken when linking from the page generated in this case, since relative links will be relative to the virtual and non-existant directory rather than the true script directory.

if ( count($userpass) == 2 ){ #this part work not for all. #print_r($userpass);die; #<- this can help find out right username and password list($name, $password) = explode(':', $userpass); $_SERVER['PHP_AUTH_USER'] = $name; $_SERVER['PHP_AUTH_PW'] = $password;

It's a piece of code , to give a piece of reflexion about simple auth , we can also cryp login and pass in db , time is here for non-replay , the code isn't finish , but it work , only for reflexion about auth mechanism

I came up with another approach to work around the problem of browsers caching WWW authentication credentials and creating logout problems. While most browsers have some kind of way to wipe this information, I prefer having my website to take care of the task instead of relying on the user's sanity.

Even with Lalit's method of creating a random realm name, it was still possible to get back into the protected area using the back button in Firefox, so that didn't work. Here's my solution:

Since browsers attach the credentials to specific URLs, use virtual paths where a component of the path is actually a PHP script, and everything following it is part of the URI, such as:

By choosing a different number for the last component of the URL, browsers can be tricked into thinking that they are dealing with a completely different website, and thus prompting the user for credentials again.

Note that using a random, unrestricted number will still allow the user to hit the back button to get back into the page. You should keep track of this number in a server-side file or database and regenerate it upon each successful login, so that the last number(s) become invalid. Using an invalid number might result in a 403 response or, depending on how you feel that day, a 302 to a nasty website.

Care should be taken when linking from the page generated in this case, since relative links will be relative to the virtual and non-existant directory rather than the true script directory.

A simple script for SSL Client Certificate authentication with a basic authentication fall-back. I use this on my site using LDAP server to check username/passwords and client certificate to user mapping.

<? // Check if and how we are authenticatedif ($_SERVER['SSL_CLIENT_VERIFY'] != "SUCCESS") { // Not using a client certificate if ((!$_SERVER['PHP_AUTH_USER']) && (!$_SERVER['PHP_AUTH_PW'])) { // Not logged in using basic authentication authenticate(); // Send basic authentication headers }}

In the previous example it will not work in IE. In order to have a single script work on both IE and FireFox (and handle the cache problem), you need to use the $_SERVER['HTTP_USER_AGENT'] variable to know which logout version to present to the user.

An full example can be seen in the url (could not post it here due to size restrictions):

or, if you don't like the idea of modifying the global $_SERVER variable directly, just use the first line and then substitute $_SERVER['PHP_AUTH_DIGEST'] in the sample code with $headers['Authorization']. Works great.

After we do auth with PHP in Apache, we miss the userid in the access.log file, unless we redefine the LogFormat to be "%{REMOTE_USER}e" rather than "%u", and upon successful authentication we do<?php apache_setenv('REMOTE_USER', $user);?>

The method described in the text does not appear to work with PHP in cgi mode and Apache-2.x. I seems that Apache has gotten stricter or introduced a bug such that you can initiate the authentication, but Apache seems to try to authenticate the browser response which always fails because it does not know what to authenticate against and the headers never get passed back to the PHP script.

It's also better to but to put the Auth-Digest-Header in a function and call it on unsuccessful authentification again. Otherwise users only have the chance to submit their username/password just one time.

In my use of HTTP Authentication, I've found that some Apache setups swap around the usual variables.Here's the fix I made so that you can still use PHP_AUTH_USER and PHP_AUTH_PW. Hope it helps someone

You need to get rid of the first 6 characters for some reason, then decode the Auth data from its base64 format. Then it's a simple matter of extracting the data. You can even pass the data to the $_SERVER variables $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW']. These are the variables that get the login data if you have PHP running as an Apache module. This is useful for mods or plugins.