[SlugBug] htaccess sessions

Jean-Michel Hiver jhiver at mkdoc.com
Mon Mar 22 12:50:03 GMT 2004


Funny you should ask - We've had that same problem with our software MKDoc.

We've solved the problem using mod_perl - I have written a paper about 
it - see below.

Cheers,
Jean-Michel.


Hacking HTTP Authentication with mod_perl.
==========================================


Introduction
------------

There are basically three ways to do HTTP authentication with mod_perl:


- Set a ticket / session cookie on the client and then use the session object
  to do the authentication.

  + very easy to implement.
  - users can reject cookies.
  - cookies have a bad name, privacy concerns.


- Set a ticket / session ID on the client by sticking the session ID somewhere
  in the URIs.

  + works with all browsers.
  - URIs become very ugly.
  - What happens with bookmarks?
  - What happens with search engines?


- Use standard HTTP authentication

  + works in all browsers
  - there is no 'log out' option
  - credentials are sent only on password protected areas, making optional
    authentication difficult.

This paper focuses on the third method, "Standard HTTP authentication", and
shows how to implement optional authentication and logout mechanisms using
nothing else but standard, plain HTTP authentication.


Optional Authentication
-----------------------

For the sake of the example, say on your site you have the simple, following
structure:

  http://www.YourSite.com/
  http://www.YourSite.com/about/
  http://www.YourSite.com/services/
  http://www.YourSite.com/products/


Say a user visits http://www.YourSite.com/. If the user is not authenticated,
you might want to display something such as:

  You are not logged in. You can [Login].


If the user is authenticated, you might want to say:

  Hello, $login.


But of course you cannot directly password-protect http://www.YourSite.com/
since it would be impossible for visitors which do not have an account to view
the site.

In order to implement this kind of optional authentication, first you write a
handler which performs a redirect as follows (or you could do this directly in
your apache config file if you prefer).

  http://www.YourSite.com/.login -> http://www.YourSite.com/


Then you password-protect the /.login location:

  <Location /.login>
    PerlAuthenHandler MyHandler::Authenticate
    AuthName "Please enter your user credentials"
    AuthType Basic
    require valid-user
  </Location>


So now when you request http://www.YourSite.com/.login, it prompts you for a
username and password, and redirects you to http://www.YourSite.com/.

If you use a packet sniffer you can also see that your browser also sends the
credentials to this address! Hurray!

However since http://www.YourSite.com/ is not password protected Apache will
still ignore the user credentials. This is quite understandable: since there
is no authentication handler, there is no way for Apache to check wether you
are really who you pretend to be. Hence Apache completely ignores the user
credentials.

However, we can use a fixup handler to do this job. This is what you get in
your Apache config file:


  <Location />
    PerlFixupHandler MyHandler::Authenticate_Opt
  </Location>
  <Location /.login>
    PerlAuthenHandler MyHandler::Authenticate
    AuthName "Please enter your user credentials"
    AuthType Basic
    require valid-user
  </Location>


Of course you need to write MyHandler::Authenticate_Opt... it looks like this:

  package MyHandler::Authenticate_Opt;
  use strict;
  use warnings;
  use Apache::Constants qw/:common/;
  use MIME::Base64;
  use strict;
  use Carp;


  sub get_login
  {
      my $r = Apache->request();
    
      my $authorization = $r->header_in ('Authorization') || return;
      $authorization =~ s/^Basic (.*)/$1/;
      $authorization = decode_base64 ($authorization);
      my ($user, $sent_pwd) = split (':', $authorization);
      return $user;
  }


  sub get_password
  {
      my $r = Apache->request();
      my %headers = $r->headers_in();
      my $authorization = $r->header_in ('Authorization') || return;
      $authorization =~ s/^Basic (.*)/$1/;
      $authorization = decode_base64 ($authorization);
      my ($user, $sent_pwd) = split (':', $authorization);
      return $sent_pwd;
  }


  sub handler
  {
      my $r = shift;
    
      my $sent_pw = get_password() || return OK;
      my $login   = get_login()    || return OK;
    
      return OK unless ($login and $sent_pw);
    
      _auth_ok ($login, $sent_pw) and do {

	  # don't forget to undef this variable with a cleanup handler or at
	  # the end of your program, otherwise you're in trouble...
          $::REMOTE_USER = $login;
      };

      return OK;
  }


  sub _auth_ok
  {
      my $login   = shift;
      my $sent_pw = shift;
      
      # your own authentication logic goes here.
  }


  1;


  __END__


As you've noticed I use $::REMOTE_USER rather than $ENV{REMOTE_USER}. I've had
problems fiddling with %ENV so I use %:: instead... not that it's much
cleaner. Use your own thing as appropriate 

So as you can see this fixup handler solves the optional authentication
problem quite nicely.

To summarize:

- http://www.yourSite.com/.logout is password-protected by an authentication
  handler to get the user to sent his credentials and redirects to
  http://www.yourSite.com/. From then on the browser will always send user
  credentials.

- http://www.yourSite.com/ is not password protected, however a custom fixup
  handler can retrieve and process the user credentials for you (provided they
  have been sent).


Logging out:
------------

HTTP authentication does not provide an easy way of logging out. This is
actually a browser bug, however with no active development from microsoft on
Internet Explorer, you can't really count on browsers very much.


However, again there is a way of getting around it. Say you wrote a custom
authentication handler which you could use as follows:

  <Location /.logout>
    PerlAuthenHandler MyHandler::Authenticate_Never
    AuthName "Please enter your user credentials"
    AuthType Basic
    require valid-user
  </Location>

The users goes onto /.logout. Since MyHandler::Authenticate_Never *always*
returns AUTH_REQUIRED, the user has no choice but to press "Cancel" on the
browser authentication dialog.

>From then on the browser will stop sending the credentials, since they were
invalid.

However that's very ugly: not only the user has to press Cancel, but also ends
on a horribe 'Authorization Required' page. Yuk. 


So instead of using /.logout, you could use /?logout=true. Basically this is the
front page except that the query string contains '?logout=true'. From then we
can do a couple of interesting things:

- In your program, when the query string contains '?logout=true', you can set
  the status to '401 Authorization Required'. For example, if you're running
  some script under Apache::Registry that's how you'd do it:

    print "Status: 401 Authorization Required\n";
    print "WWW-Authenticate: Basic realm=\"Please enter your user credentials\"\n";

- Since you're supposed to be logged out, you also want to set $::REMOTE_USER
  (you know, the variable which we set in our fixup handler) to undef.

This is getting much nicer: when the user logs out, we're serving the page
http://www.yourSite.com/?logout=true as if the user was logged out (because we
undef'd the $::REMOTE_USER variable) and there is a login box which asks you
for a new username and password.

However the user has still no choice but clicking on 'Cancel', and still
cannot log-in again. This is because no matter what the browser will always
request http://www.yourSite.com/?logout=true.


So how to get around this? The solution is to set a timestamp in the future.
This works as follows:

- /.logout sets the timestamp shortly in the future, for example:

    # timestamp 7 seconds in the future
    my $timestamp = time() + 7;

- /.logout performs a redirect to /?logout=<timestamp>

>From then on things get a little bit cleaner:

- Since the redirect is performed immediately you are almost guaranteed that
  the request to /.logout=<timestamp> will be performed before time() <
  $timestamp.

- Hence, when your program sees /.logout=<timestamp>, it checks if the current
  time is more recent than $timestamp. If that's the case, your program forces
  the client to re-authenticate:

    my $timestamp = $cgi->param ('logout');
    $timestamp and time < $timestamp and do {
        print "Status: 401 Authorization Required\n";
        print "WWW-Authenticate: Basic realm=\"Please enter your user credentials\"\n";
    };

Since the timestamp is set very shortly in the future, even if the user
changed his mind and wants to re-login, there is a great chance that by the
time he's typed his username / password the timestamp will be in the past for
the next request.

If the user is really fast at typing username and password it is non-fatal:
he'll just be re-prompted for username / password until the timestamp expires.
To the user it might look like he mistyped his password.

Of course if the user clicks 'Cancel', he becomes un-authenticated as
previously discussed.


Conclusion
----------

Cookie-less authentication with optional authentication and logging out
functionality is very possible with basic HTTP authentication and a bit of
mod_perl hacking.

The techniques presented in this paper offer many advantages:

- Your site URIs stay nice and clean
- Cookie-less environment is comforting for paranoid minds
- Compatible with command-line tools such as lynx -auth=id:pw <url>
- Very RESTful-friendly

And none of the drawbacks / showstoppers, traditionally:

- There is no optional authentication
- Users can't logout


EOF




More information about the SlugBug mailing list