The situation

Just recently at the educational institution where I work I set up a CAS server and integrated it with most of our systems, including Moodle, several WordPress instances and also our Banner installation.  We’d been using this level of integration successfully for several months in the development process while we waited for some other external projects to come into line with our single sign on initiative.  Since we use Google Apps for Education all of our enterprise applications are hosted with Google – which uses the SAML method of doing single sign on – and we had to create a bridge to translate the CAS authentication to SAML so that our users could log in once, and access everything from registration (Banner) to class information (Moodle) to online communities and library data (WordPress) as well as their email, calendars, and documents (Google Apps).

The official line

According to Google’s help documentation, the official word is to use simplesamlphp to initiate the SAML response to Google’s SAML request, so that is exactly where I started.  I set up an instance of simplesamlphp and walked through testing with the basic hard-coded username password match in the simplesamlphp configuration.

It didn’t work.

Use an RSA certificate

The problem was that I was using the wrong type of certificate.  I had followed the instructions for getting my google certificates at their help site and produced a DSA key, because our CAS server runs on Java and I anticipated that Google’s SSO would want to talk to our CAS server.  However, that’s not true.  The Google server needs to talk to our web server that hosts simplesamlphp.  When I looked up how to hook Google Apps into simplesamlphp, the simplesamlphp website had a lovely line that say, “Note: simplesamlphp will only work with RSA and not DSA certificates.”  So, one problem solved.  I changed the certificate type and my hard-coded tester worked.  If I were tied into a local database, my project would have been done.

Creating the Certificate for Google

  1. Generate a private RSA key with OpenSSL: put in a pass phrase you can remember for 5 minutes
    openssl genrsa -des3 -out login.key.secure 2048
  2. Strip the passphrase out of the private key, otherwise the service will need the passphrase before it can boot (or it will just fail entirely)
    openssl rsa -in ./login.key.secure -out login.key
  3. Create a certificate request with the new key: make sure to put in the server’s name in the Common Name field, so for the login CAS server the Common Name = login.fuller.edu
    openssl req -new -key ./login.key -out login.csr
  4. Create a self-signed certificate using the request and the key
    openssl x509 -req -days 1024 -in ./login.csr -signkey ./login.key -out login.yourdomain.tld.crt
  5. Copy the text of the cert and the RSA public key into the file at /var/www/html/googlesso/SAMLResponseOrig.php on the CAS Server.  The code gives the exact location for where to paste the text from those two items.
    1. If you followed the above directions the public key is in the file ‘login.yourdomain.tld.crt’ and should go into the PHP variable $response_params[‘x509’]
    2. The private key is in the file login.key and should go into the variable $private_key
  6. Upload the self-signed cert to Google in the Security > Set Up Single Sign-on (SSO) > Verification Certificate > Replace Certificate
    Selection_156_blank

Attempting to set up CAS with simplesamlphp

The entire extent of the information I could find on the simplesamlphp website for setting up CAS was this page.  The information is scant, and when I went to test the CAS plugin, I kept getting an uncaught exception because the CAS server could not be validated.  Since I have set up several phpCAS authentication scripts, I know that you normally set the phpCAS::setCasServerCACert() with the path to the server certificate so that phpCAS will authenticate.  Since the documentation on the simplesamlphp is so sparse when it comes to CAS, I figured I would search for the setCasServerCACert() function to see if I could hard code it in.  But, after a bit of searching and more testing, I finally decided that this was not something I wanted to debug.  I mean, I had written several bits of code that authenticated against the CAS server, so why not use my own code?

Writing it (mostly) from scratch

I assumed – and rightly so – that someone else had decided that they wanted to write their own SAML authentication piece.  Technically, what he decided was that simplesamlphp was overblown and no longer “simple” and so he pulled only the SAMLResponse piece from the simplesamlphp application and created a wrapper around it.  I downloaded the scripts, used a hard-coded test of my name on our student domain and voila! success!  (btw – Since it uses simplesamlphp, it also needs the RSA certificate.)

But clearly, using a hard-coded username wouldn’t work, so I needed to get the username from CAS and pass that to Stefan’s script.  What I did was point the Google Apps SSO settings to my new page, and then make a test SAML Request by trying to log in to http://mail.google.com/a/schooldomain.edu/acs.  Each time I ended up with an error that said my username could not be verified.  So I broke each step down, and printed out everything to the screen as it came through.  What I finally realized is that it seems that what phpCAS is doing to retain a sense of state during the forced authentication breaks the base64 encoding of the SAMLRequest by url encoding it, thereby turning the request into gibberish.  The end result was to decode the SAMLRequest, break it into an XML string, and then re-encode it just before sending all the data to the SAML Response page.  And that works.  Now all our users can move between all of our local services and all of our Google hosted services seamlessly.

The code below can also be downloaded here.  Note that you will need to download the phpCAS package from Jasig and place it in the directory where you put these scripts.

<?php
// index.php
// make sure your google apps sso page points to this index page
////////////////////////////////////
// SAVE THE SATE OF THE REQUEST
$SAMLRequest = $_REQUEST['SAMLRequest'];
$rs = urlencode($_REQUEST['RelayState']);
$incoming = base64_decode($SAMLRequest);
if(!$xml_string = gzinflate($incoming))
{
$xml_string = $incoming;
}
if (isset($_REQUEST['logout']))
{
phpCAS::logout();
}
/////////////////////////////////////
// CAS RELATED SETUP
$cas_host = 'yourcashost.edu';
$cas_context = '/cas';
$cas_port = 443|8443|etc;
require_once 'cas/CAS.php';
//phpCAS::setDebug();

// Initialize phpCAS
phpCAS::client(CAS_VERSION_2_0, $cas_host, $cas_port, $cas_context);
phpCAS::setCasServerCACert('/path/to/your/casserver.crt');
// force CAS authentication
phpCAS::forceAuthentication();
// logout if desired
$new_user = phpCAS::getUser();
$sr = base64_encode($xml_string);
header("Location: https://yoursamlscripthost.edu/googlesso/saml.php?SAMLRequest=$sr&RelayState=$rs&user=$new_user");
?>

Once the index page has processed the username via CAS, you’ll come to this page:

<?php
///////////////////////////////////////
// REDIRECT USER TO GOOGLE AFTER SUCCESSUL
// CAS AUTHENTICATION
$SAMLRequest = $_REQUEST['SAMLRequest'];
$relayState = urldecode($_REQUEST['RelayState']);
$user = $_REQUEST['user'];
// THIS NEEDS TO BE CHANGED TO MATCH YOUR DOMAIN
$loginUser = $user."@yourdomain.edu";
/////////////////////////////////////
// SAML RESPONSE RELATED SETUP
require_once('SAMLResponse.php');
$auth = new xauth_googleapps();
$auth->login($SAMLRequest,$loginUser);
$form = <<<FORM
<h3>Redirecting you to Google</h3>
<form method="post" action="{$auth->acs_url}">
<input type="hidden" name="RelayState" value="{$_GET['RelayState']}" />
<input type="hidden" name="SAMLResponse" value="{$auth->SAML_response}" />
<input type="submit" value="Submit" />
</form>
<script>document.forms[0].submit();</script>
FORM;
echo $form;
?>

You will also need the SAMLResponse.php and xmlseclibs.php which can be found in the zip file.

Update 9/3/13

We launched last night (and into the morning), and one interesting thing that happened was that this automatically failed on our live apps domain.  It took us a bit to determine what was going on, but following each network request we were able to see that the $xml_string in the index.php file was adding in line breaks during the Base 64 encoding – and subsequent routing through CAS apparently.  What threw us off the trail for a while was that the ACS URL for our test domain (https://mail.google.com/a/student.ourdomain.edu) worked just fine, while the live apps (https://mail.google.com/a/ourdomain.edu) failed to properly pass the ACS_URL to our saml.php code.  Our best guess is that Google’s test server and production servers were sending every so slightly different XML (one had a space in it).  The following code changes worked for us:

index.php

$sr = urlencode(base64_encode($xml_string));

saml.php

$SAMLRequest = urldecode($_REQUEST['SAMLRequest']);

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.