REST based authentication

Summary

It is possible to distinguish between requests from authenticated and non-authenticated users by relying upon a standard feature of browsers, namely that they include the Authentication HTTP header even for unprotected portions of the site.

Portions of the site that really require authentication will work correctly, because the Authentication header is included. Portions of the site that do not need authentication, can use the name of the authenticated user mentioned in the HTTP authentication header to do personalization.

This page shows how to achieve these effects with all the common browsers and almost pure Apache 2.2 (it probably works with the 1.3 series as well).

Authentication is hard

Authentication is one of the hardest issues when developing software. Because if you got even one bit wrong, your solution is no longer secure. And your reputation may go down with it. So why do web developers insist on developing their own security? Why not use HTTP authentication which is probably far more secure than most programmers will ever be able to develop themselves?

Common authentication issues

When people talk about user authentication with a web browser, the following topics tend to come up:

Authentication should be optional: unauthenticated users should be able to use (part of) the site. Authentication gives access to additional features. Login screen should be customizable. User should be able to logout. User should be logged out automatically after a certain time period.

It is generally believed that cookies are necessary to support these features. RESTafarians however like to change light bulbs without cookies, so are we stuck? We can answer that question with a firm denial. As far as I know, Jean-Michel Hiver was the first to show an alternative technique that relies only on HTTP authentication and doesn't use cookies. As the code he presents is somewhat outdated (it doesn't work with the latest Apache and mod_perl module for example), is not complete, and he doesn't clearly state why his approach works, I have also brought his code up-to-date. The actual solution chosen by Jean-Michel's company works fine however, you can see it in action at MKDoc. I also improved upon his log off functionality. His solution does not work with Internet Explorer and Opera, but I found some techniques that give acceptable results.

The solutions presented here use only voodoo, so sacrifice your chickens and let's go!

Optional authentication

Use case: if the user is authenticated, display a personalised greeting message or show a personalised version of the site. If the user is not authenticated, display a login screen or generic version of the site.

The solution to this problem is to let the browser include the Authentication HTTP header, even when the site doesn't require authentication. Because the site doesn't need authentication, anonymous access is still allowed. But if the user has authenticated himself, we can use this header to personalise the site.

Let's first have a look at the Authentication header. It looks something like this:

Authorization: Digest username="myname", realm="My Site", nonce="uJPKOnEOBAA=7c7d02a5ed5e214c7bb21f28bdc577f422ca0b47", uri="/index.html", algorithm=MD5, response="adefccac270ee8110639460a0fb90f4c", qop=auth, nc=00000004, cnonce="c87ceaa8c4e9d15c"

It's the username parameter in which we are interested. But how can we trick a browser to send us this header? Let's first create the page that anyone who isn't logged on will see. The body is something like this:

<body> <p><a href=".login">Please login</a> to use this site.</p> </body>

(By the way, a full demo of the code in this document is available under a subdirectory of this page, just follow this link to see the login page. Username is “myname”, password is “test”)

Before looking at how login works, you will need to make sure Apache is setup correctly. The most important setting is that a .htaccess file can override any security setting. Your httpd.conf should therefore include the following settings:

AllowOverride All Options FollowSymLinks

in its VirtualHost or Directory directive.

The login link points to a URL which we will catch in .htaccess . That is easier than the approach chosen by Jean-Michel which requires some Perl hacking.

The .login entry in our .htaccess is this:

<Files .login> AuthType Digest AuthName "My Site" AuthUserFile /var/www/html/rest/login/passwd Require valid-user </Files>

So there is no .login file, it's just a URL that will trigger authentication because accessing this URL requires a user to be authenticated.

AuthType is set to Digest. I chose to use Digest authentication because it solves some important security concerns compared to the Basic setting used by Jean-Michel. It will not come as a surprise that the largest software vendor on this planet has released another piece of software that requires, due to the sheer market presence of that vendor, everyone else in the world to work around a bugs it contains when using digest authentication. Make absolutely sure you have this directive in your configuration or .htaccess file:

BrowserMatch "MSIE" AuthDigestEnableQueryStringHack=On

Note that if you use SSL, basic authentication is as secure as the digest variant, because the password is now impervious to sniffing as well.

The AuthName directive is used to present the site name to the user for which he has to specify a user name and password. The password file itself is in the passwd file. You add users this file with the htdigest utility like this:

htdigest -c passwd "My Site" myname

It subsequently asks for a password for the user "myname". The passwords you create are stored in a plain text file. To achieve scalability above more than a few dozen users, Apache also supports storing users and their passwords in a DBM file or in a relational database.

The statement Require valid-user is necessary in order for Apache to send the WWW-Authenticate header to the browser. It indicates that this file or directory is protected. As soon as any user tries to access this URL, he or she will need to provide a user name and password. In our case the user name is "myname" and the password is "test".

At this point it is important to point out that the login URL must point to something that is in or below every directory that wants to profit from optional authentication. For example if the the login was a directory like /login the user would be authenticated, but the authentication header would never be send if the user went to /other/part . At least not with FireFox 1.5.0.1. You would think that adding URLs to the AuthDigestDomain setting would do the trick, but that setting doesn't seem to be widely supported right now.

Assuming that the Authentication header is present, is not a trick that just happens to work due to some browser quirks. RFC 2617 states:

The Authorization header may be included preemptively; doing so improves server efficiency and avoids extra round trips for authentication challenges.

This advise is probably followed by all decent browsers. It is at least by the browsers I've tested this approach with.

Careful readers might have seen that AuthDigestDomain was not present in the .login . As mentioned support for AuthDigestDomain seems to be lacking in browsers, but we can rely on particular feature when it is not present. As RFC 2617 states:

If this directive is omitted or its value is empty, the client should assume that the protection space consists of all URIs on the responding server.

Exactly the feature we're looking for when basing personalisation on HTTP authentication!

Ok, that's enough about Apache's HTTP authentication settings for now. Back to the browser which just has popped up a login dialog box if you followed the .login URL. If you provide the correct user name and password, Apache will next try to send you the .login file. But that would be rather pointless. After a successful login the user should be redirect to the home page and this should now indicate that the user has logged in. We can do the redirect from within the .login section in the .htaccess by adding these statements to it:

<Files .login> ... RewriteEngine on RewriteCond %{REMOTE_USER} !="" RewriteRule ^.*$ /index.html [R] </Files>

In the first line we turn on Apache's rewrite engine. The next statement is a conditional: if the REMOTE_USER environment variable is set, execute the following rewriting rule. REMOTE_USER is set if a browser has sent an Authenticate header and if Apache has successfully authenticated the user. The rewrite rule simply directs the user to the site's home page.

Although the user is now redirected to the home page, the default page (see above), index.html , is still served saying the the user needs to login. But when the user has been authenticated, we want to serve a different page. This can be solved either dynamically or statically. In your scripts you could emit different HTML when the REMOTE_USER environment variable is set. In our example we simply serve a different page, in this case one that let's a user logout.

Serving a different page if a user has been authenticated sounds familiar. And indeed, just like with login's .htaccess section we can accomplish this with another rewrite:

RewriteEngine on RewriteCond %{REMOTE_USER} !="" RewriteRule ^index.html$ authenticated.html

Again we turn the rewriting engine on and check if REMOTE_USER has been set. If so, we serve authenticated.html instead of index.html .

However, if you try this, you will notice that this does not work, even when you were just authenticated!. The reason is that the environment variable REMOTE_USER does not have a value. Apache sets this only when these three conditions are met:

The browser sends the Authenticate header. Apache is asked to validate the user. For example the .htaccess file contains the Require valid-user directive. Validation succeeds.

The browser sends the Authenticate header, but Apache doesn't have to validate the user, so it does not set REMOTE_USER . So we're stuck or what? One solution is to make sure REMOTE_USER is indeed set. That's a solution that I explore in the alternative section. But there's even an easier solution, one that doesn't require any mod_perl skills at all! Just vodoo...

We know that the Authorization HTTP header is send, so why not use that? That leads to the following solution that nicely redirects a previously authenticated user to a personalised page:

RewriteEngine on RewriteCond %{HTTP:Authorization} username=\"([^\"]+)\" RewriteRule ^index.html$ authenticated.html [L]

If you try this, you will be properly redirected to the authenticated.html page. Where you can logout if you so wish. And that is the subject of the next section.

There are some other solutions as well that make the authenticated user available, see the alternative solutions page.

User should be able to log off

Use case: user has logged on to our application from a public terminal. Before leaving the public terminal, the user wants to log off the session so a subsequent user cannot continue his session.

In order to understand the solution let's first have a look at what is happening after we have authenticated. After we have authenticated, the browser has stored the supplied user name and password. It needs to store these values so it can use them when the supplied nonce value has become stale for example. This is no different from any other authentication scheme. The client needs to store some kind of ticket that can be used for a certain amount of time or automatically renewed if the client remains active.

The purpose of a log off is to make a browser forget the supplied user name and password. If the browser does not forget the user name and password, the browser will always be able to re-authenticate. Relying upon HTTP authentication is the most secure way of accomplishing this. There are many more eyes viewing HTTP authentication issues than the usual roll-your-own cookie based authentication scheme.

Unfortunately, no browser does offer an out-of-the-box logout feature for their HTTP authentication. This is a lamentable failure in their implementation and unfortunately not something the poor web developer can solve. For certain browsers, such as FireFox, there is an extension that adds logout. But most FireFox users will not have this extension installed.

It is therefore commonly thought, see for example HowToLogOff and HTTP Authentication and Forms, that it is not possible to allow a user to log off his HTTP authenticated session.

However this is not true. You can ‘trick’ a browser to forget the credentials. Unfortunately the solution is browser dependent. The approach that should work is forcing a user to re-authenticate by sending by sending a 401 response to a request. This should popup the login dialog box. If the user presses cancel here, the browser should discontinue sending the Authenticate header to the server. This technique works for Mozilla/FireFox and Opera (with a twist), but not for Internet Explorer.

We can force a re-authenticate by sending the browser to a URL called .logout for example. This resource will always return the 401 response, thereby forcing the user to press the Cancel button after which the credentials will be forgotten. And the “Authorization Required” page will be displayed. Jean-Michel observes: “However that's very ugly.” But that can be fixed as well.

But let's see how such a thing can be done with the usual .htaccess trickery. First a portion of the home page that is shown when a user has been authenticated:

<p>You're authenticated. But you may <a href=".logout">logout</a>.</p>

What happens when the browser tries to access .logout ? We require authentication when something tries to access .logout , but we do two things differently:

We specify a user name that does not exist. So a user cannot authenticate and we respond to the browser's request with 401 Authentication Required. We specify an authentication realm that is either equal or different from our the authentication real used with .login . We need an equal realm for Mozilla/FireFox to forget the authentication credentials, but we need a different realm to trick Opera unfortunately. We do not mention AuthDigestDomain so the change should apply to all URLs on our server.

Assuming our browser is FireFox, .logout can be implemented as below. This will not work for Internet Explorer, it will give a weird error message, it will not even popup the login dialog box. Opera will simply not forget the credentials if you press cancel in the login dialog box. But trusted FireFox works fine:

<Files .logout> AuthType Digest AuthName "My Site" AuthUserFile /var/www/html/rest/login/passwd Require user nonexistent </Files>

But it is indeed not friendly at all. Popping up a dialog box is OK. It helps the user to understand that he is really logged off and can walk away from the public terminal now, because the next user will have to type in the credentials or press cancel. But what if our user just wants to log on again with either the same or different credentials? That should be supported nicely as well.

So a logout can be either of two things: logout, but also logging again. Logout and login are actually the same thing! Once this concept is grasped, the solution falls into place quite nicely:

The logout link is the same as the login link. We add a special query string to it so login can deny access on the first attempt, forcing the browser to display the login dialog box.

The “first attempt” is the issue here. It sounds like we need to store state on the server. That's always a big no-no as things start to get complicated. It's much better if the client can store the state. But how to do that? If we let the browser ask for a URL of the form logout?first_attempt=true and based on the presence of first_attempt always return 401 Authorization required to the browser, the user will never be able to authenticate. The browser will simply popup a box asking for the proper user name and password and submit this again and again to our URL, i.e. logout?first_attempt=true . So how do we get around this and avoid storing state on the server?

And here Jean-Michel had his second brilliant idea: use a time stamp. If the time stamp is less than a few seconds ago we assume it's the user's first attempt, and we force him to logout. If the time stamp is past the current time, we assume he typed in his password and wants to login again.

We can solve this problem with almost pure voodoo (I've written the non .htaccess parts in Perl, so it keeps looking like voodoo :-) ). First the logout. When the user follows the .logout link we redirect him to login, but include the time stamp. Have this handler in your .htaccess file:

<Files .logout> RewriteEngine on RewriteRule ^.*$ /.login?logout=${timestamps:0} [R,L] </Files>

The redirect to login now includes a parameter. It is important that this be an external redirect. The browser must get this exact time stamp, because that is where the state is stored!

The actual time stamp value is retrieved from a rewrite map. A rewrite map is something that accepts a key and returns a string. It can be a hashed file. But in our case it is a simple program that returns the current UNIX time (seconds since the Unix Epoch) + 7 seconds. That will give the browser 7 seconds to receive the redirect, send it to the server again, which will respond by showing the login dialog box to the user.

The time stamp returning program can be written in any language, here is the Perl variant:

#!/usr/bin/perl $| = 1; while (<STDIN>) { print time() + 7, "

"; }

The program simply reads a line from standard input and writes the time stamp to standard output. The input is not important, anything will do. The resulting redirect URL is of the format .login?logout=1141933263 .

Installing rewrite maps that are programs takes some care. As they are started when Apache is started they need to be in your server config or VirtualHost directive. And make sure the RewriteEngine is turned on, else you will get the error message “map lookup FAILED” when you use the map.

RewriteEngine On RewriteMap timestamps prg:/var/www/perl/timestamps.pl

The .login link needs to handle the case where the logout query is present, so we add these rewrite rules before any other rules:

RewriteEngine on RewriteCond %{QUERY_STRING} ^logout=([0-9]+)$ RewriteRule ^.*$ /${optional-forced-logout:%1} [L]

What's happening here? First we have a conditional on the presence of the logout parameter. Due to the presence of parentheses in the regular expression, the value of the time stamp is stored in the %1 variable. We pass the time stamp to another rewrite map. Again it is a simple program that accepts the time stamp on stdin and decides if the time stamp is new enough to force a login or not.

The output of the optional-forced-logout rewrite map is one of these two values:

.force_logout_offer_login .dologin

Depending on the output of the rewrite map we redirect the user to one of these URLs. The rewrite map program can be written in any language as it is extremely simple, but the Perl version is this:

#!/usr/bin/perl $| = 1; while (<STDIN>) { my $timestamp = $_; if ($timestamp > time()) { print ".force_logout_offer_login

"; } else { print ".dologin

"; } }

It keeps on reading standard input and for every received time stamp it emits the proper redirection URL. You have to add this rewrite map to your VirtualHost or Directory directive as follows:

RewriteEngine On RewriteMap optional-forced-logout prg:/var/www/perl/optional-forced-logout.pl

So what happens when the .optional-forced-logout is received? It is treated as an internal redirect, the browser isn't aware of it, an important point. Actually, we need three redirects, depending on the browser. Remember that the authentication realm must be the same for Mozilla/FireFox, but different for Opera, and for Internet Explorer we must do something different altogether. So the rewrite engine routes us to one of these three URLs:

.force_logout_offer_login_mozilla , or .force_logout_offer_login_opera , or .force_logout_offer_login_ie .

Each does the actual logoff. The redirect voodoo looks like this:

<Files .force_logout_offer_login> RewriteEngine On RewriteCond %{HTTP_USER_AGENT} (MSIE) RewriteRule ^.*$ /rest/tada/.force_logout_offer_login_ie [L] RewriteCond %{HTTP_USER_AGENT} (Opera) RewriteRule ^.*$ /rest/tada/.force_logout_offer_login_opera [L] RewriteRule ^.*$ /.force_logout_offer_login_mozilla [L] </Files> <Files .force_logout_offer_login_mozilla> AuthType Digest AuthName "My Site" AuthUserFile /var/www/html/rest/login/passwd Require user nonexistent </Files> <Files .force_logout_offer_login_opera> AuthType Digest AuthName "Not My Site" AuthUserFile /var/www/html/rest/login/passwd Require user nonexistent </Files> <Files .force_logout_offer_login_ie> RewriteEngine on RewriteRule ^.*$ /rest/tada/logged_out.html? [R] </Files>

The force log off resource simply requires a user name that does not exist in the password file. So the server responds to the browser that authentication has failed. The browser shows the login dialog box. Let's assume that the user types in his credentials again. If he presses OK, the browser again hits the .login?logout=123456789 URL. Let us further assume that 7 seconds have gone by. The optional-forced-logout rewrite map (see above) returns just .dologin . This is an internal redirect to:

<Files .dologin> AuthType Digest AuthName "My Site" AuthUserFile /var/www/html/rest/login/passwd AuthDigestDomain / Require valid-user # If user is authenticated, redirect to main page RewriteEngine on RewriteCond %{REMOTE_USER} !="" RewriteRule ^.*$ /index.html? [R] </Files>

By the way, .dologin is similar to the first version of .login we presented. The browser has sent us the credentials again and assuming the user has typed them in correctly, the user will be validated and redirected to our home page.

This approach works brilliantly with Mozilla/FireFox. If you find the dialog box that it displays less than brilliant, there's even a solution for that. With Opera we have a minor issue: because we have changed the authentication realm, Opera will ask for a password for that realm. If the user presses cancel, everything is fine and Opera will have forgotten the password. However, if the user enters the password, he will not be authenticated because the realm is "Not My Site", but the user is directed to a URL that requires a password for “My Site”. So a 401 is returned, and Opera will popup another login dialog box. User enters the correct password, this time for “My Site” Validation now proceeds correctly.

As you might have seen above, in case the browser is Internet Explorer, we redirect the browser straight to logged_out.html . The problem with Internet Explorer is that forcing re-authentication doesn't work: if you try it for the same authentication realm, this browser will come up with a weird error message. If you try a different authentication realm, it will not forget the credentials. So that's why we redirect to logged_out.html . The purpose of this page will be discussed below. This page contains some JavaScript trickerly that will force Internet Explorer to forget its credentials. Unfortunately this trick only works for version 6 SP1 or higher. When this page is loaded, we execute this JavaScript and now even Internet Explorer has logged off:

<script language="javascript" type="text/javascript"> var agt=navigator.userAgent.toLowerCase(); if (agt.indexOf("msie") != -1) { document.execCommand("ClearAuthenticationCache"); } </script>

Note that this log off solution works against three common browsers. Jean-Michel's logout does not work for Internet Explorer and Opera. If you try this out on the live MKDoc site, it says you're logged off, but you are not actually. I've not tested it myself, but others have told me that this solution also works on Safari 2.0.4 (419.3).

There are two remaining issues to be discussed. What happens when the user types in his user name and password within 7 seconds, i.e. within our time stamp period? In that case access will still be denied. The user will probably think he mistyped his password. It's not nice, but not a very likely scenario either. If all users to your site are on a fast connection, you could lower the 7 second timeout value to further minimise occurrence of this case.

The second issue is that after the users logs out by pressing the cancel button, he gets an ugly “Authentication Required” page. How that is solved, is discussed below.

Required authentication

Pickup up a user's name without real authentication raises security concerns. It is trivial to fake HTTP headers. When this approach is followed, care should be taken to protect sensitive URLs with the proper AuthXXXX directives. That should be done anyway, so this approach won't make a site less secure. But for the unprotected data a designer should always keep in mind that although we have a user name, it might not be the actual user.

Automatic log out after a period of inactivity

Use case: if the user has not accessed the site, upon the next request the user will be asked to re-enter his or her credentials.

Banks seem to like this approach a lot. If you do not use the site for a while, they force you to log on again. I won't be showing any code how to solve this issue, but I will just be sketching how it can be done.

This is a case where the server needs to keep state unfortunately. Client state isn't reliable as it can be faked. Because the user has been authenticated (else he or she is by definition not logged in), we have a proper user name. For every access that a user does we update the last access time for that user in a database. Apache supports checking user credentials against a database, so there's no need to duplicate user information.

When we receive an access from the user that is past a certain number of seconds compared to his last access, we can force re-authentication. There are two approaches:

The simplest is to send an external redirect to browser to the .logout URL. But that requires the user to find his way back, very annoying. It would be much nicer to allow the user to proceed with the original request, after he has been re-authenticated again.

I haven't actually developed any code for the second approach, but I believe this can be done if we realise that the user's name and password hasn't changed, but what has changed is the authentication realm. The authentication realm has expired and is no longer valid. The user needs to supply the username and password for the new realm. Here's how this could be done:

Update the last access time of the user. Force an internal redirect to something like the .force_logout_offer_login URL. It's important that this URL has the new authentication realm. The new authentication realm is user dependent, so this needs to be stored in the database. Because the realm has changed, the server will send a 401 to the browser. The browser will popup the login dialog box. The user must now authenticate or press cancel. If the user authenticates again, the browser will retry the original request. Because the last access time has been updated, the request is considered valid. Because the resource is protected by authentication, authentication will be done and succeed if the user has supplied the correct credentials. If the user presses cancel, he will be properly logged off in all browers, because even if the browser continues to remember the credentials, the authentication realm has expired so the user must login before being able to proceed to secured pages.

Apache's mod_authn_dbd seems to be very suited to do this as it allows you to lookup the username and realm in a database.

Personalised login page

Use case: when the user logs on, do not force the browser to present the default login, but present a nicely formatted page where the user can enter his credentials.

With the help of AJAX pure, HTTP authentication will even support this! The steps are simple:

Put a user name and password input box on the form. Put a “Login” button on the form. On click it should not post the data, but call a JavaScript function. The JavaScript function asks the server if the user name and password are correct. If so, the function access our original .login URL using the XmlHttpRequest object. This object allows you to pass the user name and password (Note: Opera currently does not support this). Because the user name and password are correct, the browser will not popup a dialog box. If they are incorrect, the browser will do this, even for an asynchronous call! (Note: Opera will not popup a dialog box, just return a 401 status code!) Because the server responds that authentication has been successful, the browser will now continue to send the Authentication HTTP header to every URL to the directory of the current URL and all its subdirectories, as discussed before.

Problem solved. Or nearly. The big issue is item 3. If you send your user name and password as clear text over the internet, you defeat the entire purpose of using HTTP authentication. To solve this issue properly you need to implement an entire Challenge-Response scheme. This is nicely explained by Eric Lippert in a four part series Eric. This is the non-trivial part and is therefore left as an exercise to the reader. But let's look at a few other parts of the code. First the onclick action of the button:

login( document.getElementById('username').value, document.getElementById('password').value)

On click the button calls a function called login() , and passes in the user name and password. The login() function is this:

function login (username, password) { request.open("GET", ".login", true, username, password); request.onreadystatechange = onLogin; request.send(null); }

The request object is an instance of XmlHttpRequest. This asynchronous call simply calls .login with the proper user name and password. If you want to check the user name and password before making this call, you have to implement step 3 with all the responsibilities to get it correct.

The callback function is onLogin . This checks if we have received a 200 response and in that case redirects us to the home page:

function onLogin () { if (request.readyState == 4) { if (request.status == 200) { window.location="index.html"; } } }

Which should indicate that we have logged on, because in our .htaccess we redirect to authenticated.html in that case. Note that you should have FireBug disabled, else you still will get the standard login dialog box!

I have ambivalent feelings about custom login screens. Not only are they hard to get secure as discussed before, but how careful is the browser with storing this user name and password? Does it get stored in the cache? In unprotected areas of the computers memory? The browser doesn't know this data is sensitive. I have the feeling that the browser will be more careful with the user name and password supplied by HTTP authentication than with two input fields on a page.

And lastly: it is probably very hard for a rogue application or third party to fake the default HTTP authentication login screen. This should be impossible really. That way a user can be sure he is using trusted authentication with proper security controls. Faking a custom login screen is much easier.

Forgot password page

Use case: if a user can no longer remember his password and gives up authentication by pressing the cancel button in the authentication dialog box, display a page where the user can request a new password.

This use case is just one of all the essential use cases a well-behaving HTTP-authentication application will have to cover. Because when a user presses the cancel button, the application knows the user cancelled the login, but not why. Didn't the user remember his password anymore? Did the user want to logout? Or had the user more pressing issues right now, but wants to login at a later time?

So after pressing the cancel button we have to display a page that allows a user to do the following:

Allow the user to log on again, perhaps under a different identity. Let the user request a new password.

In addition this page should confirm the user's current status, i.e. that the user is logged off. Perhaps reiterating the point, after authentication only two things can happen:

The user supplies the correct user name and password. In that case the user will be given access to the authorised page. The user gives up the authentication process by pressing cancel. In that case the error page is displayed.

The browser will never give up authentication until one of these two things have happened. When HTTP authentication fails, Apache serves the error document for the 401 status. This can be overridden in your .htaccess with the ErrorDocument directive:

ErrorDocument 401 /logged_out.html

The URL must start with a slash, else Apache thinks it is just an error message. The error document must contain the links to allow a user to login again or request a new password.

Caching issues

Because of all the URL rewriting and serving of different content when a user is authenticated, you must make sure caching doesn't get in the way. For this example it is enough to disable sending any expiry information and disabling any intermediate caches. Put these lines in your .htaccess to make it all work:

ExpiresActive Off Header append Cache-Control "no-cache"

ExpiresActive only needs to be set to Off when it has been set to On and when the Expires module is loaded of course. The “no-cache” value makes sure no caching takes please anywhere.

For a real site you probably want to be very cache friendly, but while testing you want to avoid tracking errors that are just due to you being send a non-updated page.

Conclusion

Cookie-less authentication is quite possible while retaining all the features that are usually associated with choosing cookie-based authentication in the first place.

I appreciate comments and questions about this solution. And because I don't have access to Safari I like to get a note from those users if this approach works for their browser as well. And I appreciate it even more if you let me know if you think I have made a fatal flaw in my reasoning or code.

Appendix: my .htaccess file

The full .htaccess file I used in my examples:

# Turn off expires in case mod_expires has been loaded and it has been # activated. ExpiresActive Off # And disable caching, always validate against server. Header append Cache-Control "no-cache" # Support the biggest software vendor on the planet which produces # the biggest load of crap BrowserMatch "MSIE" AuthDigestEnableQueryStringHack=On ErrorDocument 401 /logged_out.html # Only when user tries to access .login, ask user for authentication <Files .login> # If login is actually a logout, force logout and offer login # but only when the timestamp is within range. RewriteEngine on RewriteCond %{QUERY_STRING} ^logout=([0-9]+)$ RewriteRule ^.*$ /${optional-forced-logout:%1} [L] AuthType Digest AuthName "My Site" AuthUserFile /var/www/passwords/passwd.digest Require valid-user # If user is already authenticated, redirect to main page RewriteEngine on RewriteCond %{REMOTE_USER} !="" RewriteRule ^.*$ /index.html? [R] </Files> # We come here if a timestamp is present, but has expired <Files .dologin> AuthType Digest AuthName "My Site" AuthUserFile /var/www/passwords/passwd.digest Require valid-user # If user is authenticated, redirect to main page RewriteEngine on RewriteCond %{REMOTE_USER} !="" RewriteRule ^.*$ /index.html? [R] </Files> # Force a logout by forcing a re-authenticate which always fails # We get send here by a .login when the timestamp has not expired. <Files .force_logout_offer_login> RewriteEngine On RewriteCond %{HTTP_USER_AGENT} (MSIE) RewriteRule ^.*$ /.force_logout_offer_login_ie [L] RewriteCond %{HTTP_USER_AGENT} (Opera) RewriteRule ^.*$ /.force_logout_offer_login_opera [L] RewriteRule ^.*$ /.force_logout_offer_login_mozilla [L] </Files> <Files .force_logout_offer_login_mozilla> AuthType Digest AuthName "My Site" AuthUserFile /var/www/passwords/passwd.digest Require user nonexistent </Files> <Files .force_logout_offer_login_opera> AuthType Digest AuthName "Not My Site" AuthUserFile /var/www/passwords/passwd.digest Require user nonexistent </Files> <Files .force_logout_offer_login_ie> RewriteEngine on RewriteRule ^.*$ /logged_out.html? [R] </Files> # Redirect user to .login but include a timestamp # Must be an external redirect, else we come here again and again even # if user supplies the correct password. <Files .logout> RewriteEngine on RewriteRule ^.*$ /.login?logout=${timestamps:0|1} [R,L] </Files> # if user name known, show authenticated page # We have several solutions each differing in complexity. RewriteEngine on #RewriteCond %{REMOTE_USER} !="" #RewriteRule ^index.html$ authenticated.html [L] RewriteCond %{HTTP:Authorization} username=\"([^\"]+)\" RewriteRule ^index.html$ authenticated.html [L] #RewriteRule ^index.html$ authenticated.html?user=%1 [L] #RewriteRule ^index.html$ %1.html [L]

Appendix: my configuration

I've tested my sample code against the following browsers:

FireFox 1.5.0.1: full support. Microsoft Internet Explorer 6.0.2800.1106 (version 6 SP1): full support (don't forget to turn on the workarounds in Apache to work around IE's Digest authentication bug). Mozilla 1.7.11: full support. Opera 8.52: custom logon does not work.

Apache 2.2 was built with:

./configure --prefix=/usr --bindir=/usr/sbin --sbindir=/usr/sbin --localstatedir=/var/run/httpd --enable-module=so --sysconfdir=/etc/httpd/ --enable-rewrite --enable-expires --enable-headers --enable-include --enable-deflate --enable-disk-cache --enable-cache --enable-auth-digest --enable-authn-dbm --enable-authz-dbm --enable-ssl --with-ssl=/usr/local/ssl --enable-cgi --enable-headers --enable-vhost-alias --enable-log-forensic

I used Perl 5.6.1 for the RewriteMap examples I presented.

In alternative solutions I have used mod_perl 2.0.