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 2)

by Andrew Sterling Hanenkamp
06/14/2007

In the first part of this article, I shared my implementation of the Cental Authentication Service (CAS) protocol using the Jifty framework, CAS+. That article covered the basic details of how this single sign-on (SSO) system was implemented to include basic login and how a user's credentials are passed back to another web service. This covered most aspects of the CAS 1.0 protocol.

However, the CAS protocol provides much more than robust single sign-on. CAS 2.0 provides additional features for authentication by proxy. By using this extension, a web service may pass identity credentials on to additional services allowing services that are not directly web accessible, such as IMAP, SMTP, or other applications. CAS+ implements these extensions as well and this article will share, and this article will explain how those are implemented in Jifty.

Why Proxy?

This question was the biggest obstacle for me while trying to understand how the CAS protocol works. You can login, get a service ticket, validate that ticket, and then request a new special kind of ticket, and then there are callbacks and yet another set of tickets to worry about. Why?

The answer is that a certain web service might want to grant the user access to other services, especially to those that are not web-based. For example, let's say you have an intranet portal for your employees to check on vacation time, current benefits, email, etc. Your portal provides a central interface for everyone to get this information, but the vacation time is actually stored in a performance management system, the benefits are pulled from your company's financial services partner, and your email is pulled from your IMAP server. Each of these systems aren't going to hand over any information without knowing who the information is being handed to.

By using CAS to proxy the authentication, they can all be sure of the identity of the individual requesting the information while you are still keeping the actual credentials private to the CAS system.

Protocol Additions

The process of authentication by proxy is identical up to the point of service authentication. I described the first part of this process in Part 1. Now, let's take a look at how the process is modified to handle proxy authentication. I'm going to break the process down into three parts to help simplify the explanation.

figure 1

Login

The first phase of proxy authentication in CAS is exactly the same as typical authentication, login. The steps in this phase are:

  1. User requests a restricted page. The client web service will redirect the browser to the CAS server for authentication. The redirect contains the service parameter giving the URL to return to upon authentication.
  2. User logs in or has her previous login checked and verified. The browser contacts the login form of the CAS server and the CAS server either requests login or determines the user has already logged in and skips to the end of the next step.
  3. The CAS server verifies the user's credentials and sends the user back to the web service with a Service Ticket. On successful login (or if the user logged in previously in this session), the CAS server redirects the user to the URL given in the original service parameter. This redirect as a Service Ticket attached in the ticket parameter.

figure 2

Validation

Once the user has logged in and the user is redirected back to the restricted page with the Service Ticket, the client web service has to validate the service ticket. This is similar to non-proxy authentication, but an extra step is involved.

  1. The user requests the original page with the Service Ticket. The browser passes the ticket parameter. Until the web client returns the response, the browser no longer takes part in the transactions that occur.
  2. The web service contacts the CAS server directly to validate the Service Ticket. The web service passes the service URL, the Service Ticket, and a callback URL to the CAS server. To implement proxy authentication, the web service must provide a special URL just for the CAS server to contact. The CAS specification also states that this URL must be secured with SSL.
  3. The CAS server contacts the PGT URL as a callback with a new Proxy Granting Ticket (PGT) and PGT IOU. The CAS server passes the Proxy Granting Ticket, which is used later to request a special kind of service ticket, called a Proxy Ticket. CAS also sends a Proxy Granting Ticket IOU, which is used to link the PGT to the username. If everything is received correctly, the callback returns success to CAS.
  4. CAS completes the validation request by returning a response containing the username and PGT IOU. This response informs the web service the identity of the logged user as well as giving the PGT IOU, which allows the web service to associate the PGT given separately with the username.
  5. The web service now responds with the contents of the requested page now that the identity has been confirmed. This assumes that the user identified by CAS has authorization to view the requested page. It's still possible that the user doesn't have the required privileges, but CAS is only concerned with authentication. Authorization is not handled by CAS.

Why the extra callback? Why couldn't CAS just pass the Proxy Granting Ticket back with the username? I presume this is for additional security. A malicious attacker must snoop two different request/response pairs going in opposite directions (and crack the encryption on both) to get a complete picture of what is happening.

However, if a malicious user can snoop just the callback communication and gain just the PGT, the attacker has all that's required to spoof access to an account, which is why the CAS protocol requires this communication be encrypted. It is very important that the client web service not betray the PGT for this reason. (And perhaps a reason for a CAS server implementation to refuse to perform proxies unless the service is pre-approved for such access.)

CAS+: The PGT callback

To perform this additional callback step, the System uses LWP::UserAgent to pass the PGT and PGT IOU in an HTTP GET request. This happens in the middle of the CASPlus::Action::Validate action:

$pgt->create(
    service_session => $service_ticket,
    callback_url    => $pgt_url,
    proxy_session   => $proxy_ticket,
);

# Contact the callback URL with pgt and pgtIou
my $response = $ua->get(
    $pgt_url 
        .'?pgt='.$pgt->proxy_granting_ticket
        .'&pgtIou='.$pgt->proxy_granting_iou
);

Here the $pgt_url holds the value passed in the pgtUrl parameter. The Validate request creates a new proxy grant session and then passes the identifiers. If the callback returns success (i.e., $response->is_success is true), the Validate action adds the PGT IOU to the XML response with the username. The response to the client web service will look something like this:

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

figure 3

Proxy

Now, the user has identified herself with the CAS server. The CAS server has passed credential information to the original web service by way of the browser and validated it directly for the web service. The web service has requested a Proxy Granting Ticket and received one via callback and linked that PGT to the user's identity. Now, the web service needs to securely pass the identity of the user on to the proxied application.

  1. The user requests a page that includes data pulled from a proxied application. As far as the user is aware, she's just clicked on a link to get information. The web service, upon receiving the request, knows that the user is now logging into an external application and proceeds to authenticate using the Proxy Granting Ticket it acquired previously.
  2. The web service requests a Proxy Ticket from CAS. To proxy authentication, the web service must first request a Proxy Ticket from CAS. It does so by contacting the CAS server and passing the name of the application that will be authenticating (the targetService) and the Proxy Granting Ticket the web service acquired earlier (the pgt). CAS responds with a Proxy Ticket, which is more or less the same thing as a Service Ticket.
  3. The web service passes this Proxy Ticket to the proxied application. The only credentials the application must provide to the proxied application is the Proxy Ticket.
  4. The proxied application validates the Proxy Ticket with CAS. The proxied application now performs a validation request that is nearly identical to the service validation performed for Service Tickets. The application passes it's service name (which must match the targetService used to request the Proxy Ticket) and the ticket. If the Proxy Ticket is valid, the CAS server returns the username.
  5. The proxied application is now authenticated and is able to return data accessible by the validated user. The web service and proxied application may continue communicating to retrieve and modify data according to their shared knowledge of the user's identity.
  6. The client web service may now return any data retrieved from the proxied application. The process is now complete. The user has accessed data from the proxied application through the web service.

In the description of this process, I've separated the request for a restricted page from the request for a proxied application. I've done this to simplify the description of this complex process, but both actions could occur within the same request.

I've also performed the request of fetching a Proxy Granting Ticket during initial service validation. However, this could wait until later as well by invoking the login process a second time to get a new service ticket and performing a new service validation request. When such actions are performed is dependent upon the needs and implementation of the client web service.

Also, I've left out the fact that a proxied application can request its own PGT using its callback URL. Then, that application can proxy the authentication of another application. Proxied authentication may be arbitrarily nested.

CAS+: Requesting a proxy ticket

How does CAS+ handle creating and returning a Proxy Ticket? Here's the rule in the CASPlus::Dispatcher that shows this:

on 'proxy' => run {
    my $proxy = Jifty->web->new_action(
        class     => 'Proxy',
        arguments => {
           pgt           => get 'pgt',
           targetService => get 'targetService',
        },
    );
    $proxy->run;

    set result => $proxy->result;
    show '/proxy';
};

Here the dispatcher sends the request to the Proxy action (CASPlus::Action::Proxy) and then response with the "/proxy" template. As with the other actions, the Proxy action spends most of the code dealing with error cases. The king work that it does is to load the proxy grant session associated with the given Proxy Granting Ticket and then creates a new proxy session record for the given targetService.

On success, the "/proxy" template will render something that looks like:

<?xml version="1.0"?>
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
    <cas:proxySuccess>
        <cas:proxyTicket>PT-IN94NF0M3A...</cas:proxyTicket>
    </cas:proxySuccess>
</cas:serviceResponse>

CAS+: Validating a proxy ticket

The only other new step in this process is that of validating the Proxy Ticket. However, the nature of this validation is practically identical to that of Service Ticket validation. In fact, the CAS protocol species that the Proxy Ticket validator also be capable of validating Service Tickets. Because of this, the ProxyValidation action is a subclass of the Validation action introduced in Part 1 of this article.

If the incoming ticket starts with "ST-", it is identified as a Service Ticket and the superclass is used directly:

if ($ticket =~ /^ST-/) {
    $self->SUPER::take_action;
    return;
}

Otherwise, the much of the same process for validating a Service Ticket is applied tot he Proxy Ticket instead. Much of the code used by the Validation action has been extracted into inherited subroutines to prevent a lot of code from being duplicated.

Most of the code that remains in the ProxyValidation action is related to the error handling messages that are specialized for use with Proxy Ticket validation.

There is, however, one major distinction between Proxy Ticket validation and Service Ticket validation in the output template, "/proxyValidate": it includes a list of proxies, which isn't part of the "/serviceValidate" template. As such, a typical Proxy Ticket validation response will look something like this:

<?xml version="1.0"?>
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
    <cas:authenticationSuccess>
        <cas:user>sterling</cas:user>
        <cas:proxies>
            <cas:proxy>https://service2/callbackUrl</cas:proxy>
            <cas:proxy>https://service1/callbackUrl</cas:proxy>
        </cas:proxies>
    </cas:authenticationSuccess>
</cas:serviceResponse>

That summarizes most of the major code additions required to make proxy authentication work.

Lots of tickets

Okay, I chose not to explain much about the models in Part 1 of this article, but there are enough "tickets" involved that I thought it might help to see the basics of how these are stored in CAS+. It also shows off the declarative syntax Jifty uses to declare its data models.

SSO session

The SSO session is stored in CASPlus::Model::SSOSession. This is the basic building block for authentication. Here is the abbreviated Jifty::DBI model declaration:

column authenticated_user => 
    refers_to CASPlus::Model::User;
column ticket_granting_cookie => 
    type is 'text', is mandatory, is distinct;
column current_valid_ticket => 
    type is 'boolean', default is 1;

An SSO Session record is created once the user has logged in. At that time, a cookie is set in the browser's cookie stash containing the ticket_granting_cookie identifier. By looking up the ticket_granting_cookie, the system can now who the user is by looking it up in authenticated_user. If the user logs out (not discussed in my articles, but part of the CAS protocol), the current_valid_ticket is changed to false and all other sessions associated with it are immediately invalid.

Service session

The service session is stored in CASPlus::Model::ServiceSession. When a web service redirects the browser to the CAS server, a service session is created once the user's identity is verified and the Service Ticket referring to this session is returned to the client web service.

Here's the abbreviated declaration:

column sso_session => 
    refers_to CASPlus::Model::SSOSession;
column service_url => 
    type is 'text', is mandatory;
column service_ticket => 
    type is 'text', is mandatory, is distinct;
column current_valid_ticket => 
    type is 'boolean', default is 1;
column expiration_time => 
    type is 'timestamp', is mandatory;

Once the client web service receives the Service Ticket, it will perform a validation of that ticket. CAS looks up the Service Ticket in the service_ticket column and returns the username associated with the authenticated_user column of the linked sso_session record. It also sets current_valid_ticket to false so that validation can't be made against the same ticket twice. Finally, if validation is attempted after expiration_time, the validation will fail (there's a 5 minute time limit between requesting the service ticket and performing validation).

Proxy grant session

The proxy grant session is stored in CASPlus::Model::ProxyGrantSession. When a callback URL is specified during Service Ticket or Proxy Ticket validation, a proxy grant session record is created. Here's the abbreviated declaration of the model:

column service_session => 
    refers_to CASPlus::Model::ServiceSession;
column proxy_session => 
    refers_to CASPlus::Model::ProxySession;
column callback_url => 
    type is 'text';
column proxy_granting_iou => 
    type is 'text', is mandatory, is distinct;
column proxy_granting_ticket => 
    type is 'text', is mandatory, is distinct;

Later, when a Proxy Ticket is requested, the system looks up the SSO session associated with service_session to make sure it is still valid and then returns a new Proxy Ticket. When Proxy Ticket validation occurs, the chain of proxy_session records is read and returned in the response.

Proxy session

The proxy session is stored in CASPlus::Model::ProxySession. A proxy session is created as soon as a web service or application owning a Proxy Granting Ticket requests a Proxy Ticket. Here's the abbreviated declaration:

column proxy_grant_session => 
    refers_to CASPlus::Model::ProxyGrantSession;
column proxy_ticket => 
    type is 'text', is mandatory, is distinct;
column service_identifier => 
    type is 'text', is mandatory;
column current_valid_ticket => 
    type is 'boolean', default is 1;
column expiration_time => 
    type is 'timestamp', is mandatory;

During validation, the proxy_grant_session is used to determine if the parent SSO session is still valid and to fetch the username of the authenticated user. The service_identifier is the same as the service session's service_url, except that they don't have to be URLs since proxied applications often don't have a web address. The current_valid_ticket and expiration_time are used in the same way as they are in the service session.

What Else?

The CAS 3.0 version of this protocol doesn't really change the mechanisms so much as it creates places where the messages can be modified or added to. Many CAS administrators need features like "single sign-off" where each service is notified immediately by callback when user's log out. Some want CAS to handle additional data storage of profile information, roles and permissions, or Access Control Lists. CAS 3.0 provides mechanisms for adding customizations for this purpose. I'm (slowly) working to add facilities for these to the CAS+ implementation as well.

Distributed Authentication

Another related method of authentication that is growing in popularity is distributed authentication. Protocols, such as OpenID, allow a user known on one system to login to systems all over the web without the other systems having an explicit trust relationship. This is nice because it allows the user even more control over authentication and allows a user who has an account on multiple systems to login to all of those systems just by logging into their blog or corporate web site.

I'm hoping to add OpenID support to CAS+ at some point as well. It could act as both a consumer of OpenID data (signing in elsewhere via an associated OpenID account is enough to sign-in to any service trusting your CAS server) and as a provider where other OpenID servers can use CAS+ to identify users.

CAS is a very simple protocol for all the features it provides. I was pleasantly surprised to find out how easy it was to implement and am somewhat surprised that there aren't more CAS server implementations available. It is very easy. I think this experiment also illustrated the inherent flexibility of Jifty. At least one developer has tried to convince me that Jifty is not very flexible and doesn't work well in unusual cases. I think this has helped to prove to Jifty is a very workable and robust framework.

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.