Brice Stacey home

Configure EZProxy to Authenticate Against Voyager ILS for WorldCat Local / Navigator

I just finished configuring EZProxy's user authentication for our implementation of WorldCat Navigator. EZProxy has traditionally been used to allow off-campus users access to licensed materials. However, EZProxy was recently aquired by OCLC and has been bundled into WorldCat Navigator for authentication. To do this, EZProxy was extended to generate UserObjects which contain a user's information and could be passed around like a session identifier. I'm excited for the possibilities that this may allow a single-signon (SSO) for all OCLC services such as ILLiad.

One of my biggest complaints against EZProxy is the lack of documentation. To improve on that, I thought I'd share my results.

Healey Library has been using text files ever since we began using EZProxy. That involves querying our patron database and dumping username:password pairs into a text file. We did this every hour to try and keep our systems in sync. To complicate things, the text file was generated on one machine and then FTPed to the EZProxy server. I have found this to be rather problemnatic as you might expect.

Overzealous First Ideas

I saw WorldCat Navigator as an opportunity to correct this. I first created the most botched workflow for authenticating people on the fly. It involved a script that would parse a single user.txt file to generate UserObjects. The trick however was that each time a person logged in, this file would first be overwritten to contain only that person's data. I published it to a listserv of 80 people prescribed the task of implementing EZProxy for their instituion. Some people liked it, some people didn't. It worked, but had some flaws for high traffic institutions.

An anonymous source (to which I owe a lot of thanks) by means of Jeff Greer offered up an alternative solution that used EZProxy's III authentication scheme. EZProxy's implementation of III's PatronAPI is generic enough that you can mimic the same style of output as PatronAPI and have it be parsed by EZProxy. Awesome. Since this isn't exactly Innovative Interfaces Inc.'s PatronAPI, I've taken the liberty of coining this Faux PatronAPI.

Our Implentation of EZProxy's User Authentication

In this whole process, I also was helping to identify how to properly form a connection with Selfchk, which implements the SIP protocol for Voyager. EZProxy supports SIP as a means of authentication. So, when I discovered how to establish a connection with SIP, I started investigating how I could use it for EZProxy. I soon discovered that EZProxy's default settings append a newline '\n' at the end of each SIP statement, which is not permitted by Voyager's implementation of SIP and appropriately so (the SIP protocol clearly states that each statement should end in a carriage return '\r'). I emailed the creator of EZProxy and was told the "SupressNewLine" directive would fix this and it did.

Although Voyager's implementation of SIP is very strict with newlines, it doesn't fully implement the protocol. The SIP protocol returns patron email addresses when you send a Patron Information Request, however Voyager doesn't send this. However, SIP is very nice in that it properly returns a patron's circulation status taking into consideration all possible circulation policies, which would take mountains of effort to replicate.

Ultimately, we used SIP to first authenticate users and populate most of the UserObject and Faux PatronAPI to fill in the remaining gaps (email address and epxiration date).

Here is a copy of our user.txt. Please note that we use EZProxy to authenticate both electronic subscriptions as well as WCL/NRE authentication.

::SIP
  SupressNewLine
  Host example.com:7031
  LoginUsername SIP_USER
  LoginPassword SIP_PASS
  LoginLocation SIP_LOCATION
  SIP2

  If auth:AF eq "Patron barcode not found" {
   Deny loginbu.htm
  }

  Set ParseName(auth:AE, "FMSX", "login")
  Set pass = REReplace("/[^a-zA-Z]/g", "", login:pass)
  Set lastCheck = REReplace("/[^a-zA-Z]/g", "", login:surname)
  Set passCheck = REReplace("/[^a-zA-Z]/g", "", login:middleName . login:surname)
  If !AnyWild(pass, "*" . lastCheck) || !AnyWild(passCheck, "*" . pass) {
    Deny loginbu.htm
  }

  #SIP 4 (card reported lost)
  If auth:4 eq "Y" {Deny loginbu.htm}

  Set url = Coalesce(login:url, cookie:url)
  If url =~ "/userObject/i" {
    # This is an NRE request, only allow authorized patron groups.
    If auth:PT !~ "/(UND|GRAD|STAFF|FAC|TEST)/i" {
      Deny loginbupg.htm
    }

    Set session:groupNumber = INSERT_GROUP_NUMBER
    Set session:instNumber = INSERT_INST_NUMBER
    Set ParseName(auth:AE, "FMS", "session")
    Set session:uid = auth:AA
    Set session:category = auth:PT
    If auth:0 eq "N" {
      Set session:bannedInRemoteCirculation = "N"
    }
    If auth:0 eq "Y" {
      Set session:bannedInRemoteCirculation = "Y"
      Deny loginbudel.htm
    }
    # Get email and expiration date from ILS using Faux PatronAPI
    If UserFile("user_iii.txt") {}

    # NRE Auth is ended by the follow Stop. EZProxy stops here.
    Stop
  }
  # Only the following patrons are allowed to access our online subscriptions
  If auth:PT !~ "/(UND|GRAD|STAFF|FAC|UMBONLINE|TEST)/i" {
    Deny loginbupg.htm
  }
  Accept
/SIP

The above file runs the Faux PatronAPI via the UserFile('user_iii.txt') function. Here is our user_iii.txt:

::III
  Host example.com:80
  Set session:dateFormat = "MM-DD-YYYY"
  Set session:expiryDate = auth:expiryDate
  Set session:emailAddress = auth:email
/III

Faux PatronAPI URL Rewriting

Faux PatronAPI works by making a request to http://example.com/PATRONAPI/0123456789/dump where 0123456789 is the patron's barcode. Thinking of urls as addresses to files and folders on the internet, you would have to create a folder called PATRONAPI and populate it with a folder for every barcode and populate each of those folders with a file "dump" that contains the necessary data. No thanks. Instead, we use mod_rewrite and rewrite the url to a php script with the barcode passed in the query string.

The following can be put in .htaccess or httpd.conf or one of the other million places Apache configuration directives can be placed.

RewriteEngine on
RewriteRule ^PATRONAPI/([a-zA-Z0-9\.]*)/dump$ /path/to/fauxpatronapi.php?barcode=$1 [L]

This will redirect all requests for http://example.com/PATRONAPI/0123456789/dump to http://example.com/fauxpatronapi.php?barcode=0123456789.

Writing a PHP Script to Emulate PatronAPI Formatted Output

We assume now that the request has successfully been redirected to your script. The script then connects to the Voyager ILS using an Oracle database connection, queries the barcode, fetches the patron, and returns the necessary data. In our case, it only returns their email address and expiration date. The data must be returned in the format "[key]=value". These values can then be accessed in EZProxy using the auth namespace, i.e. given the following response the patron's email address could be retrieved by auth:email:

[expiryDate]=02-27-10
[email]=username@example.com

More information about EZProxy's III authentication scheme can be found at OCLC's EZProxy website.

Here is our copy of fauxpatronapi.php:

<?php
  // Database configuration
  $username = '';
  $password = '';
  $host = '';
  $port = '1521';
  $sid = '';

$dsn = <<<DSN
  (DESCRIPTION =
    (ADDRESS_LIST =
      (ADDRESS = (PROTOCOL = TCP)(HOST = $host)(PORT = $port))
    )
    (CONNECT_DATA =
      (SID = $sid)
    )
  )
DSN;

  $conn = oci_connect($username, $password, $dsn);
  if (!$conn) {
    $e = oci_error();
    print htmlentities($e['message']);
    exit;
  }
  
  // mod_rewrite has already sanitized this
  $barcode = $_GET['user'];
  $sql = <<<SQL
    SELECT
      PATRON_ADDRESS.ADDRESS_LINE1 as email, 
      PATRON.EXPIRE_DATE as expires
    FROM 
      (PATRON INNER JOIN PATRON_BARCODE USING(PATRON_ID)) 
      LEFT JOIN PATRON_ADDRESS USING(PATRON_ID)
    WHERE 
      UPPER(PATRON_BARCODE.PATRON_BARCODE) = UPPER('$barcode') AND
      PATRON_ADDRESS.ADDRESS_TYPE = '3'
SQL;

  $stid = oci_parse($conn, $sql);
  if (!$stid) {
    $e = oci_error($conn);
    print htmlentities($e['message']);
    exit;
  }
  
  // Execute SQL query
  $r = oci_execute($stid, OCI_DEFAULT);
  if (!$r) {
    $e = oci_error($stid);
    echo htmlentities($e['message']);
    exit;
  }
  
  // Fetch result first result.
  $patron = oci_fetch_object($stid);
  
  echo '[expiryDate]=' . date("m-d-Y", strtotime($patron->EXPIRES)) . "\n";
  echo '[email]=' . $patron->EMAIL. "\n";
?>

Putting It All Together

Once all these pieces are put together, everthing should work. Just a quick note: Oracle drivers are not installed in php by default so if you intend to use a php script you will need to install them which can be tricky. It's not necessary that you use a PHP script and can always rewrite the url to anything you want.

Cheers!