Hacking 401 Authentication into PHP iCalendar

While hanging out with Adam tonight, I spent some time working on adding 401 authetication to PHP iCalendar. This was not as straight forward as I initially thought.

401 authentication (also referred to as Basic or .htaccess authentication) is the little Username/Password box that pops up on some web sites. It is the most common way to secure web content. A lot of tools that use web content (such as iCal) support 401 authentication and since tasks doesn’t have any built-in security, most people secure it with 401 authentication.

Prior to tasks 1.8 (and the 1.7.5 betas), PHP iCalendar has read a ‘tasks.ics’ file in its ‘calendars’ directory that is created by tasks. This method has a number of drawbacks, the biggest two are:

  1. The iCalendar was only updated when you clicked on the Calendar button in tasks (or if you set up a CRON or Psuedo-CRON job to update it on some regular interval).
  2. People had to know how to change permissions on the ‘tasks.ics’ file so that it was writeable by the web server.

The benefit (which I forgot about) was that it was reading a file from a local directory, and the same 401 authentication that you put in for tasks applied to PHP iCalendar as well (assuming you didn’t move the ‘phpicalendar’ directory from it’s default location).

In tasks 1.8, I solved this by creating a dynamic iCalendar file (http://www.example.com/tasks/ics.php). PHP iCalendar has the ability to display a :scare: web cal :/scare: , and it was happy to load in the dynamic iCalendar. What I overlooked in my testing was that now instead of loading the iCalendar as a file from a local directory (within the same 401 authentication realm), I was loading it as an external resource which again required authentication. Unfortunately, the latest release of PHP iCalendar doesn’t support 401 authentication.

I didn’t want to go back to updating the ‘tasks.ics’ file, so I need to make PHP iCalendar capable of 401 authentication. The solution is pretty straight forward:

  1. If you try to load an iCalendar and get a 401 authentication required error, catch the error and prompt the user for their username/password.
  2. Repeat this until you don’t get the 401 error anymore (either we’re in, or we still need a valid username/password).
  3. Then when we ask for the iCalendar again, we can pass the username/password to the web site hosting the iCalendar.

Right off the bat, I found some trouble. The current version of PHP iCalendar showed a warning message like this when you tried to load an iCalendar that required 401 authentication:

Warning : fopen(http://www.example.com/tasks/ics.php) [ function.fopen ]: failed to create stream: HTTP request failed! HTTP/1.1 401 Authorization Required in …/tasks/phpicalendar/functions/ical_parser.php on line 70

From this, I thought the fopen() function must be returning an error message that I can check to know that I need to supply 401 authentication for this file. Not so. The fopen() function either succeeds or returns a value of ‘false’. This didn’t help me much since it doesn’t tell me why it failed.

Ah-ha, but Adam suggested we use all that cool error handling stuff in PHP that I never use. Sure, I bet I can do it with that, right? Um, nope (or at least I wasn’t successful with it).

We tried using set_error_handler() to see if there was different error number for a 401 authentication failure or an error message being generated like the one being output to the browser. Unfortunately, the error number returned as ‘8’ for all of the failed fopen() commands we tried (wrong address, 401 authentication required, etc.) and never returned any error message.

Then we tried the debug_backtrace() method, but with the same results: error number ‘8’ and no error message. Argh! How can it produce an accurate warning message that it prints to the screen, but not return something that a developer can work with?

I did get the 401 authentication working. It successfully gave the user the Username/Password prompt and used it to open the 401 authenticated iCalendar. The only thing left was to catch the darn error message.

Rather frustrated, Adam and I got to talking about other projects and ideas. A little while after he left I sat back down in front of the problem again and it hit me: It’s simple. I can buffer the warning message that is output to the screen and stuff it into a variable as a string. Then, if the fopen() function has failed, I can check to see if the 401 warning message that is output to the screen is what I caught in the buffered warning message.

Voila, that was it!

I’m sending the patch to the PHP iCalendar developer mailing list for review, but at least in my testing it works. Very, very satisfying.

This post is part of the project: Tasks Pro™. View the project timeline for more context on this post.