February 24, 2008

.htaccess - Redirect to SSL (HTTPS) before Basic Authentication

I prefer running a site in HTTP only mode when there are no confidential information transferred (username, password, credit card number, etc.). It saves some of the CPU time because there is no need to do data encryption. But I strongly recommend to use HTTPS mode for confidential information exchange between a web browser and a web server.

I was facing a situation where I had to authenticate a user on a Apache web server, which provided HTTP as well as HTTPS connection. By default a web application running on that server was accessed only by HTTP. I had no access to the Apache's configuration (no root access), what would not be a problem, if I wanted to do just a .htaccess and a .htpasswd based basic HTTP authentication without anything else. It's pretty easy then. Just create a .htaccess file, with contents like this:

AuthName 'Enter your Username and Password:'
AuthType Basic
AuthUserFile /var/www/myweb/.htpasswd
Require valid-user

and a .htpasswd file (man htpasswd) which will contain usernames and particular passwords.

Then put the .htaccess file into a directory which you want to be protected by username/password.

You can even customize it by adding a FileMatch property to require credentials validation only when accessing some files:

AuthName 'Enter your Username and Password:'
AuthType Basic
AuthUserFile /var/www/myweb/.htpasswd
Require valid-user
<FilesMatch "(attach|edit|manage|rename|save|upload|mail|logon|.*auth).*">
      Require valid-user
</FilesMatch>

As I said above, I preffer HTTPS connection when confidential information are transfered over an IP network (in this case username and password). So the thing I wanted to do, was first to redirect a web browser to the HTTPS site and just then request the credentials. This was a point where I've got into a botleneck. If an authentication procedure is defined for a directory or a file, the authentication has higher priority then a redir command (mod_rewrite - redir). So the user is first authenticated, then moved to the HTTPS site and then authenticated once again. The problem is that the first athetincation is transferred in HTTP cleartext which is definitelly not secure.

AuthName 'Enter your Username and Password:'
AuthType Basic
AuthUserFile /var/www/myweb/.htpasswd
Require valid-user
<FilesMatch "(attach|edit|manage|rename|save|upload|mail|logon|.*auth).*">
      RewriteEngine on
      RewriteCond %{HTTPS} !=on    # If the connection is not HTTPS then apply the next Rewrite rule
      RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI}  [R,L]
      Require valid-user
</FilesMatch>

After some hours spent with configuration and RTFM, I found a hack which is maybe not the ideal solution, but it's pretty good and it's working.

There is an Apache configuration command which can be used in .htaccess as well. If the "SSLRequireSSL" command is specified for a directory or a file and it's accessed with a connection which is not SSL secured (HTTPS), it will generate a 403 error code and an error message will be sent to a web broswer. The "SSLRequireSSL" command has higher priority then the authentication itself, so it will generate this error code always when the connection is not SSL secured.
So far it looks good, the problem is that a user will just see an error page and he is still not automatically redirected to the HTTPS connection. A workaround is a bit tricky. You can define your own custom error documents, which are displayed when an error code is thrown. You have definitely seen these fancy custom error documents for the 404 - Page Not Found error code.

So the workaround was to use the ErrorDocument property in the .htaccess file. A custom page defined in ErrorDocument is called when the error code is thrown. The page itself gets information about the original request, so you can write a custom error page in some server side scripting language and generate some "special" events. I created a perl cgi script (there was no PHP support for that site) which redirects a browser to the HTTPS site which is exactly what I wanted in the beginning. So here is the .htaccess file:

AuthName 'Enter your Username and Password:'
AuthType Basic
AuthUserFile /var/www/myweb/.htpasswd
Require valid-user
<FilesMatch "(attach|edit|manage|rename|save|upload|mail|logon|.*auth).*">
      SSLRequireSSL
      ErrorDocument 403 /bin/move.pl
      Require valid-user
</FilesMatch>

and the move.pl file goes here:

#!/usr/bin/perl -T
use CGI qw(:standard);

$path = "https://$ENV{'SERVER_NAME'}$ENV{'REQUEST_URI'}";
if ( $ENV{'SERVER_PORT'} == 80) {
    print "Status: 302 Moved\n";
    print "Location: $path\n\n";
}
else {
    print "Content-type: text/html\n\n";
    print "How did you get here???";
}

With this combination, if the files defined in the FilesMatch directive are accessed with the HTTP only connection, an 403 error code is thrown by the SSLRequireSSL, which is handled by the ErrorDocument property. The /bin/move.pl cgi perl script is called which will then redirect a web browser to the HTTPS site. Furthermore, if the files defined in the FilesMatch directive are accessed with the HTTPS connection, a user is requested to authenticate himself with his username/password.

It's maybe not the best solution, but it's working, and it's enough to have .htaccess definitions enabled.

Enjoy!