ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


CAS+: Single Sign-On with Jifty (Part 1)

by Andrew Sterling Hanenkamp
05/31/2007

Single sign-on (SSO) means that one user ID and one password are entered one time to allow passage from one system to another without interruption. [1] In the realm of web development, this means you can log in on one site and then use the same credentials while moving from one domain to the next. This gets interesting because neither cookies nor JavaScript are allowed to directly share such information across domains. This means that the web applications must perform a little extra footwork to get this communication done. You also need to be concerned that you aren't giving too much information away too easily, so it's also important to allow the user to have a say in the matter during these transactions.

My employer, Boomer Consulting, is building a number of web-based tools to be used by our clients. These tools are being built on different platforms. For example, our main web site, two client service sites, and a support site will be running separate installations of Drupal. Meanwhile, we're also building some custom tools to aid our consultants and clients in Jifty. We also use other tools such as Best Practical RT for issue tracking and client correspondence. Eventually, as our services expand, we will want to integrate our e-commerce platform and possibly even support third-party applications with business partners. All of these require some amount of authentication. We don't want our clients (or our own staff) to log in every time they move from one site to the next. Login is tedious, and remembering multiple passwords is not an efficient use of our brains. By implementing SSO, we can use a single login page to grant access to every affiliated site. That way, our clients don't really need to be aware that they are moving from one platform to another. It makes for a more seamless user experience.

Once you know the general technique, it's actually pretty easy to implement a client-server single sign-on architecture. However, there are several existing SSO solutions out there for web development. One of the most popular such protocols is the Central Authentication Service (CAS), developed by Yale. I've chosen to use this protocol because I can take advantage of existing connectors that are compatible with the various platforms we're using for our web projects. It also provides a common protocol that our partners will be able to implement more easily, using already canned connectors.

This article will outline the CAS server I've developed using Jifty. Learning how CAS works was a tedious process because there's a lot of specialized knowledge involved. Here, I attempt to discuss a CAS server implementation in terms that will be more readily accessible to someone already familiar with Perl. Hopefully, you'll be able to take the principles here to develop your own CAS integration, servers, or clients; or even to roll your own SSO service more easily in Perl.

Central Authentication Service

The CAS protocol is an especially nice SSO interface because it addresses many of the potential security pitfalls. For example, it prevents login replay attacks; it prevents a user from being able to log in as another if the original user inadvertently passes a login token in a pasted link; and it provides advanced features like proxy authentication.

The basic process of logging in to a CAS server is as follows:

  1. Go to a restricted page on a client web service. The first time you visit a restricted page prior to logging in, the web service owning the page will redirect you to the login page. The login page is located on the CAS server. That is, the service will send an HTTP redirect response rather than the page itself (since it currently doesn't know if you should be able to see it). The redirect will contain a special parameter, named "service," which should contain the URL of the original page you are trying to view.
  2. The browser will connect the client to the CAS server. Here, the user is presented with a login screen. The user fills in a username and password (or other login credentials if you use client-side certificates or something else) and clicks the Login button, which contacts the CAS server again with these credentials.
  3. The CAS server will attempt to verify the credentials. If the given username and password are not valid, the user is returned to the login screen again. If they are valid, CAS sends back an HTTP redirect response. This returns the browser to the original restricted page that was passed to the CAS server originally in the "service" parameter. In addition, it will attach a parameter to the redirect, called "ticket," that contains a unique identifier, called the Service Ticket.
  4. The browser will return to the original restricted page. The client web service that owns the original page receives the request for that page again, but this time with a Service Ticket attached in the "ticket" parameter. The service initiates its own HTTP connection (this time not through the user's browser) to contact the CAS server directly. It asks the CAS server if the given Service Ticket matches the service URL it originally sent. If CAS agrees, then it replies with the username of the person logging in. The web service can then determine if the owner of that username may access the page, and will return it if so.

That's the CAS protocol in a nutshell. The remainder of this article concerns the specifics of the implementation, which will reveal more about how this actually works.

CAS+

I've dubbed my implementation of the CAS protocol, CAS+ (or CASPlus for the actual Perl module name). The original CAS server implementation (and the only significant implementation that I'm aware of) is written in Java using the Spring Framework and served using a J2EE application server. I decided to implement my own CAS server using the CAS Protocol Specification on the CAS web site for a few reasons:

Whenever possible, I like to multipurpose a project so that I get benefits on several levels out of the effort. This is the case with CAS+.

Jifty platform

I chose Jifty as my platform because I am now very familiar with it, I like it, and I am contributing to the project. Implementing the code took less than a weekend for the main components and a little more than a weekend to test and debug. This is, I believe, both a testament to the excellence of the Jifty platform and the simplicity of the CAS protocol.

Jifty is the brainchild of Best Practical Solutions, which hosts Hiveminder as both a pretty nice to-do list builder and the platform's main demo. I've built CAS+ using a development branch of Jifty, the virtual-models branch. It may not be entirely compatible with the release version of Jifty available from CPAN. Jesse Vincent, the chief monkey wrangler on Jifty, is working to bring this branch into the trunk, so I hope this won't be the case for long. However, the core functionality may work with the CPAN release. At least most tests passed when I last built against the CPAN release, but some of the advanced features (those not directly related to the CAS protocol) will certainly not work.

As of this writing, my implementation's main deviation from the standard regards support for SSL. I believe it should work with SSL for the most part, but I have not yet fully tested any of it, so I cannot confirm it. Also, the implementation does not necessarily require SSL in places where it is supposed to (according to the protocol definition) either, which is a potential security flaw in the current implementation. This is an area that I will be improving when I can get to it.

Getting CAS+

If you would like to, fetch and work with CAS+ to try it out. You'll first need to install Jifty. If you want to work with the experimental branch that CAS+ is made for, you may run something like the following on Linux or in the Mac OS X terminal:

svn checkout \
    http://svn.jifty.org/svn/jifty.org/jifty/branches/virtual-models \
    jifty
cd jifty
perl Makefile.PL
make
make test
make install

Jifty has a large number of CPAN dependencies. Most, if not all, of these dependencies should install automatically. If you have trouble, see the Jifty web site on how to get help with the installation. If you do not have SQLite installed, you will probably want to do so for easy testing of Jifty and CAS+.

Next, you need to install CAS+. To do so, you will need to run these additional commands:

svn checkout \
    http://svn.jifty.org/svn/jifty.org/apps/CASPlus/trunk \
    CASPlus
cd CASPlus

If everything has gone as planned and SQLite is installed, you can now run:

bin/jifty schema --setup
bin/jifty server

The first command builds the database and the second starts the test web server. Next, you should be able to contact the CAS+ server on your local machine at http://localhost:8889/. Of course, you can't log in yet because there are no users entered. At this time, the administration interface provided by Jifty may or may not allow you to add users due to recent bugs in CAS+ (this is still not a mature service), but if you entered the usernames and passwords directly into the users table of the CASPlus database using the command-line interface of SQLite, you can test login as well.

I don't recommend you use this service for production purposes at this time, but I would welcome additional contributors if you are interested in it.

Jifty Components

Before I go too far into the description of the code and process, let me give you a crash course in the pieces that make up Jifty. Here are the most important ones:

This is a pretty typical arrangement for MVC web frameworks.

Dispatcher

The key to understanding most Jifty applications is in the dispatcher. Basically, the Jifty dispatcher determines what views to display for any given request made by a client. Jifty has a declarative syntax for building the rules, which makes it particularly easy to determine what's going on. Because of this, I've been able to define one rule per action step in the CAS protocol, since each step can be uniquely identified by the dispatcher.

The Jifty dispatcher for CAS+ is named CASPlus::Dispatcher. (The dispatcher is always named App::Dispatcher for all Jifty applications where "App" is the name of the main application class.)

Login check

The first step the CAS server is aware of is a request for login. This can happen either as a direct request from the user (i.e., he directly requests the /login URL from the server) or by a redirect from a web service when it wants to verify the user's identity. In Jifty, this looks like the following:

on GET 'login' => run {
    my $service = get 'service';
    my $renew   = get 'renew';
    my $gateway = get 'gateway';
    # Check for existing login
    my $action = Jifty->web->new_action(
        class     => 'LoginCheck',
        arguments => {
            service => $service,
            renew   => $renew,
            gateway => $gateway,
    },
    );
    $action->run;

 # Deal with $action->result...
};

The first line declares that any GET request coming to "/login" should be handled by running the LoginCheck action (CASPlus::Action::LoginCheck), which checks to see whether the user already has a valid login with the system.[2] I normally don't specify that it has to be a GET in my Jifty dispatchers, but in this case it was convenient to separate the GET from the POST since POST is used for login submission.

The next three lines fetch query string parameters. The "service" parameter may hold a URL. This is the URL to redirect to upon successful (or in some cases, failed) login. The "renew" parameter may be set in order to force the user to log in again. This can be useful if a service wants to reconfirm that the user is still the correct person for additional security. If this is set, the LoginCheck action will always fail because a new login is required by the service, even if the user already has valid login credentials. The "gateway" parameter is used by a service just to perform a quick check for login.

The actual action carried out by LoginCheck is complex, but mundane. I'll summarize by explaining the base cases and then the exceptions.

Those are the two typical cases. If you were to implement your own SSO system, these might be the two basic cases for your service and you wouldn't necessarily need to worry about the exceptions. However, CAS implements some special features that allow for additional flexibility.

In summary, a service requests a login by redirecting the user to "/login" with at least the "service" parameter set. If the user has a login cookie (TGC), the browser is redirected back to the URL set in the "service" parameter with an additional "ticket" parameter attached containing a service ticket. If the user does not have a TGC, the user will be shown a login screen and asked to log in.

Login form

The first time the user reaches the "/login" URL, he will be shown a login form (share/web/templates/login). Once filled, the form is submitted and processed. This is handled by this dispatcher rule:

on POST 'login' => run {
    my $username = get 'username';
    my $password = get 'password';
    my $lt       = get 'lt';
    my $service  = get 'service';
    my $warn     = get 'warn';
   
    # The login screen uses a standard action, find it
    my $login = Jifty->web->new_action(
        class     => 'Login',
        moniker   => 'login',
        arguments => {
            username => $username,
            password => $password,
            lt       => $lt,
            service  => $service,
            warn     => $warn,
        },
    );
    $login->run;
   
    # handle $action->result success or failure...
};

This time, the dispatcher handles POST requests to "/login" by running the Login action.

The Login action (CASplus::Action::Login) makes its decisions on the basis of five different parameters submitted with the form. The "username" and "password" parameters are exactly what you would expect, the username and password submitted by the user. CAS does not require password authentication, but it is the form most web developers and end users are familiar with. CAS+ currently only supports password authentication, but other forms of authentication may be added in the future.

The "lt" parameter is an interesting one that is worth some elaboration. It represents the Login Ticket (LT), a unique identifier placed as a hidden parameter within every CAS login form. These are represented in CAS+ by the LoginAttempt model. A login, whether successful or not, may only use an LT for one submission. The purpose of this parameter is to prevent someone from performing what is called a "replay attack." Say you use a public terminal to log in to CAS, do your work, and then log out of CAS, but your browser session remains open. A malicious user could use the Back button of the browser to go back to the login form and repost it (since POSTs are often cached with the history on most browsers) and gain access to your resources. However, because a Login Ticket may never be used more than once, the attack is thwarted most of the time.[3] This may not be quite the problem it once was with older browsers, but it is still an interesting security precaution to be aware of.

The "service" parameter is the URL of the web service requesting the user's identity. This URL is put into the form so that the user (who does not yet have a session within CAS) may be redirected back to the web service upon successful login. The "warn" parameter is set when the "Warn me when logging into additional services" checkbox is checked. This is the setting that allows the user to be notified any time another web service attempts to gain access to the user's identity.

The Login action processes the most important of these parameters in the way you would expect. If the "lt" has already been used or is not one the CAS server has issued, the login will fail. If the "username" and "password" don't match a user record, the Login action will fail. On failure, the dispatcher returns the user to the form with an error message.

If the "username" and "password" match a record in the users table, a cookie (the TGC) is set for later reference to the user's session. A TGC record is created in the database (in the SSOSession model), which is used to remember the user's session information for later reference. After successfully logging in, the Login action will perform additional actions. If the user checked the "warn" checkbox, a note is made in the session record to warn the user on future LoginCheck actions.

Finally, if "service" is specified, a Service Ticket (ST) is generated (and stored in the ServiceSession model). A service ticket is an identifier attached to the user's login session that associated the "service" URL with the session. During the next phase of CAS authentication, the web service uses this identifier to validate the user's login claim and learn the user's identity.

On success, the dispatcher will redirect the user back to the "service" URL with the Service Ticket attached if the "service" parameter was set. The Service Ticket is added to the "service" URL in the "ticket" parameter. If no "service" URL has been given, the dispatcher just shows a page stating that login was successful.

Service ticket validation

Once the CAS server has verified the identity of the user--either by an explicit login or by checking that the user has already logged in and has a valid login session already in place--the CAS server will send the user back to the web service with a Service Ticket set in the "ticket" parameter. To complete the transaction, the web service requesting the identity of the user must then finish the transaction by contacting the CAS server directly.

By passing the communication up to this point through the user's browser, the user is allowed to control the transaction to some extent. If she doesn't want the web service requesting authentication to know her identity, she is able to stop the transaction. It also provides a straightforward mechanism for presenting the login page for verifying her credentials. At some point, however, the web service must be able to contact the CAS server directly or else the user could easily pretend to be someone she is not. This is one reason why the Service Ticket is passed rather than directly passing back the identity or credentials. The other reason for the Service Ticket is that it prevents the web service from ever gaining direct access to the user's credentials. That is, the web service consuming the user's identity never knows her password, only that the CAS server vouches for her identity.

The next step then, is for the web service to contact the CAS server directly to verify that the Service Ticket belongs to a valid user and to find out what the identity of that user is. The web service does this by using its own web client to directly contact the CAS server via the "/serviceValidate" page.[4] In CAS+, this is handled in the dispatcher as follows:

on 'serviceValidate' => run {
    my $validate = Jifty->web->new_action(
        class     => 'Validate',
        arguments => {
            service => get 'service',
            ticket  => get 'ticket',
            pgtUrl  => get 'pgtUrl',
            renew   => get 'renew',
        },
    );
    $validate->run;
   
    set result => $validate->result;
    show '/serviceValidate';
};

Here, the Validate action (lib/CASPlus/Action/Validate.pm) is called and then the "serviceValidate" template is used to return the response.

The "service" parameter here is what it has been all along, the URL of the service requesting login. This must be the exact URL used in the redirect, and it serves to help validate the authenticity of the web service. The "ticket" parameter is the Service Ticket that was passed back through the redirect (or at least that the user has claimed was passed in the redirect). Again, this must be the exact value that was passed back by CAS. The "pgtUrl" is used in proxying authentication, which will be discussed in Part 2 of this article. If a web service doesn't need to proxy authentication, which would be the typical case, then this parameter wouldn't be given. And the "renew" parameter is set if the service wants to verify that the user did just log in. This is used as the other half of the Login Renewal process discussed above.

The Validate action checks to see that a Service Ticket matches the given service URL and that it is associated with a valid login session. If the service sets the "renew" parameter, the Validate action performs the additional check that the Service Ticket being tested was the result of a fresh login (probably forced using the "renew" parameter to "/login"). If any of these tests fail, the Validate action returns failure.

Furthermore, there are two additional tests that must pass. First, the Service Ticket being validated must not be too old. Typically, "too old" means that it was issued more than five minutes ago. In CAS+, this time period is configurable. If the Service Ticket is too old, it is invalid and Validate will fail.

Second, the Service Ticket can only be validated once. If the Service Ticket being checked has already been validated once before, then the Validate action also fails. This latter test is important in case a person copies a link and sends it to someone else, and that link happens to include a Service Ticket parameter. In that case, the web service would attempt to validate the Service Ticket for the other person a second time and fail, which would probably result in a new redirect to login. If the other user is already logged in, then an immediate redirect back to the source with a new Service Ticket would be made, and the other user may not even be aware of the transaction happening that allows her to see the page correctly anyway.

If all of these tests succeed, then the Validate action returns success.

The dispatcher passes both success and failure on to the "serviceValidate" template (share/web/templates/serviceValidate). As of this writing, all the templates are written using Mason, which is the original templating system available with Jifty.[5]

<?xml version="1.0"?>
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
% if ($result->success) {
    <cas:authenticationSuccess>
        <cas:user>
            <% $result->content('username') %>
        </cas:user>
% if ($result->content('proxy_granting_ticket')) {
        <cas:proxyGrantingTicket>
            <% $result->content('proxy_granting_ticket') %>
        </cas:proxyGrantingTicket>
% }
    </cas:authenticationSuccess>
% } else {
    <cas:authenticationFailure 
            code="<% $result->content('code') %>">
        <% $result->error %>
    </cas:authenticationFailure>
% }
</cas:serviceResponse>
<%args>
$result
</%args>
<%init>
$r->content_type('application/xml');
</%init>

You can see that the response is in XML, and that the Validate action sets several variables on the result object that are used to tailor the response. On success, a message like the following is returned.

<?xml version="1.0"?>
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
    <cas:authenticationSuccess>
        <cas:user>sterling</cas:user>
    </cas:authenticationSuccess>
</cas:serviceResponse>

The web service can parse this message to know that the validation was successful and to learn the user's login name. The CAS protocol does not specify any other information about the user, just the name. This name could be used to grab information from an LDAP server or another external service if additional information is required. The CAS 3.0 protocol also specifies extension mechanisms for adding additional details about the user to the protocol. The CAS+ implementation I have written will implement a mechanism for sharing additional attributes about the user as well.

On failure, a different message is returned. For example, if the Service Ticket were invalid because a renewal was requested, but the Service Ticket was not gained from a fresh login, the web service would get the following message:

<?xml version="1.0"?>
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
    <cas:authenticationFailure 
            code="INVALID_TICKET">
        Service ticket ST-DHF81OKF08100JGC192389HNFN92VN3ZX 
        is not a renewal.
    </cas:authenticationFailure>
</cas:serviceResponse>

The response includes both a regular identifier to help the service automatically identify and respond to the problem and a textual error that can be used for logging to help the administrator diagnose problems later. (I highly recommend NOT giving CAS error messages to the end user.) You can see the CAS Protocol Specification for information on the various codes and their meanings.

Putting It Together

This server may now be used by a client web service to determine the identity of a user. This is done when the web service redirects the user to CAS to check for login. The CAS server asks the user for credentials, either in the form of a username and password or in a Ticket Granting Cookie, which is used to resume an existing session. Once the CAS server is satisfied that the user is who he claims to be, it redirects the user back to the originating web service (possibly without the user being aware of the transaction taking place). The web service receives this new request with the Service Ticket attached and then uses that ticket to verify and retrieve the identity of the user.

All of this takes place in just a few steps and really only requires three URLs to be implemented on the server side (though some of the decisions behind each URL can be complicated). I've presented the basics of that implementation here as I've done it in Jifty.

What's Next

So far, I've covered the really key features of how to implement the Central Authentication Service server in Jifty. What I've covered so far are the features provided by the CAS 1.0 protocol (though, I've covered validation using the CAS 2.0 implementation, which provides most of the same features but with a nicer response format).

As of CAS 2.0, additional features were added to the protocol to cover proxied authentication. With proxy authentication, you gain the ability to provide authentication to non-web services that are being proxied by a web service. For example, if you have a portal that provides access to an IMAP mail server, your portal can use CAS for login and then use CAS to indirectly pass identity information to the IMAP server in a way that the IMAP server can directly verify. I will be covering this process in Part 2 of this article.

[1] http://nosheep.net/story/single-sign-on-definition

[2] Normally, Jifty runs actions automatically on the basis of the forms submitted, but that won't work for this case, because I'm attempting to duplicate a protocol that doesn't include the monikers and action parameters that Jifty normally does. However, Jifty was flexible enough to allow me to do this without any difficulty.

[3] Only most of the time, because a browser might be implemented to allow the replay using a new LT. The CAS protocol authors have noted that at least some versions of Safari have this problem.

[4] This is assuming the CAS 2.0 protocol. In CAS 1.0, the URL was "/validate" and had a completely different response format. A nice thing to note about the CAS+ implementation is that both "/validate" and "/serviceValidate" are handled using the same action, Validate, but a different response template handles the difference in the protocol version responses.

[5] Jifty has recently added support for a new template system called Template::Declare and there has been discussion of adding support for the Template Toolkit.

Andrew Sterling Hanenkamp is a proud Kansan and spends most of his time hacking Perl, his web site, avoiding yard work, and with his wife and son.


Return to ONLamp.com.

Copyright © 2009 O'Reilly Media, Inc.