RubyCAS-Client

Author

Matt Zukowski <matt AT roughest DOT net>, Ola Bini <ola.bini AT ki DOT se>, Matt Walker <mwalker AT tamu DOT edu>

Copyright

© 2006 Karolinska Institutet, portions © 2006 Urbacon Ltd.

License

GNU Lesser General Public License v2.1 (LGPL 2.1)

Website

rubyforge.org/projects/rubycas-client and code.google.com/p/rubycas-client

RubyCAS-Client is a Ruby client library for Yale’s Central Authentication Service (CAS) protocol.

CAS provides a solid and secure single sign on solution for web-based applications. When a user logs on to your CAS-enabled website, the CAS client checks with the CAS server to see if the user has been centrally authenticated. If not, the user is redirected to your CAS server’s web-based login page where they enter their credentials, and upon successful authentication are redirected back to your client web application. If the user has been previously authenticated with the CAS server (with their “ticket” being held as a session cookie), they are transparently allowed to go about their business.

This client requires a CAS server to talk to. You can obtain a Java implementation of such a server as well as more info about the CAS protocol here: www.ja-sig.org/products/cas

This CAS client library is designed to work easily with Rails, but can of course be used elsewhere.

Installing

You can always download the latest version of RubyCAS-Client from the project’s rubyforge page at rubyforge.org/projects/rubycas-client. However, probably the easiest way to install CAS support into your Rails app is via the plugins facility:

./script/plugin install http://rubycas-client.googlecode.com/svn/trunk/rubycas-client

Alternatively, the library is also available as a gem, which can be installed by:

gem install rubycas-client

If your Rails application is under Subversion control, you can also install the plugin as an svn:external, which will ensure that you always have the latest version of RubyCAS-Client:

./script/plugin install -x http://rubycas-client.googlecode.com/svn/trunk/rubycas-client

Please contact the developers via the RubyForge page if you have bug fixes or enhancements you would like to contribute back.

Examples

Using RubyCAS-Client in Rails controllers

Note that from this point on we are assuming that you have a working CAS server up and running at an https URI.

Somewhere in your config/environment.rb file add this:

require 'cas_auth'
CAS::Filter.cas_base_url = "https://login.example.com/cas"

You will also need to specify the server name where your CAS-protected app is running (i.e. the hostname of the app you are adding CAS protection to, not the CAS server):

CAS::Filter.server_name = "yourapplication.example.com:3000"

Then, in your app/controllers/application.rb (or in whatever controller you want to add the CAS filter for):

before_filter CAS::Filter

That’s it. You should now find that you are redirected to your CAS login page whenever you try to access any action in your protected controller. You can of course qualify the before_filter as you would with any other ActionController filter. For example:

before_filter CAS::Filter, :except => [ :unprotected_action, :another_unprotected_action ]

Once the user has been authenticated, their authenticated username is available under request.username (and also under session[:casfilteruser]). If you want to do something with this username (for example load a user record from the database), you can append another filter method that checks for this value and does whatever you need it to do.

A more complicated example

Here is a more complicated configuration showing most of the configuration options (this does not show proxy options however, which are covered in the next section):

CAS::Filter. = "https://login.example.com/cas/login"  # the URI of the CAS login page
CAS::Filter.validate_url = "https://login.example.com/cas/serviceValidate"  # the URI where CAS ticket validation requests are sent
CAS::Filter.server_name = "yourapplication.example.com:3000"  # the server name of your CAS-protected application
CAS::Filter.renew = false                      # force re-authentication? see http://www.ja-sig.org/products/cas/overview/protocol
CAS::Filter.wrap_request = true                # make the username available under request.username?
CAS::Filter.gateway = false                    # act as cas gateway? see http://www.ja-sig.org/products/cas/overview/protocol
CAS::Filter.session_username = :casfilteruser  # this is the hash in the session where the authenticated username will be stored

Note that in this example we explicitly specified the login and validate URLs instead of letting RubyCAS-Client figure them out based on CAS::Filter.cas_base_url.

How to act as a CAS proxy

CAS 2.0 has a built-in mechanism that allows a CAS-authenticated application to pass on its authentication to other applications. An example where this is useful might be a portal site, where the user logs in to a central website and then gets forwarded to various other sites that run independently of the portal system (but are always accessed via the portal). The exact mechanism behind this is rather complicated so I won’t go over it here. If you wish to learn more about CAS proxying, a great walkthrough is available at www.ja-sig.org/wiki/display/CAS/Proxy+CAS+Walkthrough.

RubyCAS-Client fully supports proxying, so a CAS-protected Rails application can act as a CAS proxy.

Additionally, RubyCAS-Client comes with a controller that can act as a CAS proxy callback receiver. This is necessary because when your application requests to act as a CAS proxy, the CAS server must contact your application to deposit the proxy-granting-ticket (PGT). Note that in this case the CAS server CONTACTS YOU, rather than you contacting the CAS server (as in all other CAS operations).

Confused? Don’t worry, you don’t really have to understand this to use it. To enable your Rails app to act as a CAS proxy, all you need to do is this:

In your config/environment.rb:

CAS::Filter.cas_base_url = "https://login.example.com/cas"
CAS::Filter.proxy_callback_url = "https://yourrailsapp.com/cas_proxy_callback/receive_pgt"
CAS::Filter.proxy_retrieval_url = "https://yourrailsapp.com/cas_proxy_callback/retrieve_pgt"

In config/routes.rb make sure that you have a route that will allow requests to /cas_proxy_callback/:action to be routed to the CasProxyCallbackController. This should work as-is with the standard Rails routes setup, but if you have disabled the default route, you should add the following:

map.cas_proxy_callback 'cas_proxy_callback/:action', :controller => 'cas_proxy_callback'

Now here’s a big giant caveat: your CAS callback application and your CAS proxy application must run on separate Rails servers. In other words, if you want a Rails app to act as a CAS ticket-granting proxy, the cas_proxy_callback controller must run on a different server. This is because Rails does not properly support handling of concurrent requests. The CAS proxy mechanism acts in such a way that if your proxy application and your callback controller were on the same server you would end up with a deadlock (the CAS server would be waiting for its callback to be accepted by your Rails server, but your Rails server wouldn’t respond to the CAS server’s callback until the CAS server responded back first).

The simplest workaround is this:

  1. Create an empty rails app (i.e. something like rails cas_proxy_callback)

  2. Make sure that you have the CAS plugin installed. If you installed it as a gem, you don’t have to do anything since it is already installed. If you want to install as a plugin, see the instructions in the “Installing” section above.

  3. Make sure that the server is up and running, and configure your proxy_callback_url and proxy_retrieval_url to point to the new server as described above (or rather, make Pound point to the new server, if that’s how you’re handling https).

That’s it. The proxy_callback_controller doesn’t require any additional configuration. It doesn’t access the database or anything of that sort.

Once your user logs in to CAS via your application, you can do the following to obtain a service ticket that can then be used to authenticate another application:

service_uri = "http://some.other.application"
proxy_granting_ticket = session[:casfilterpgt]
ticket = CAS::Filter.request_proxy_ticket(service_uri, proxy_granting_ticket).proxy_ticket

ticket should now contain a valid service ticket. You can use it to authenticate by sending it and the service URI as query parameters to your target application:

http://some.other.application?service=#{CGI.encode(ticket.target_service)}&ticket=#{ticket.proxy_ticket}

This is of course assuming that some.other.application is also protected by the CAS filter. Note that you should always URI-encode your service parameter inside URIs!

Note that CAS::Filter#request_proxy_ticket actually returns a CAS::ProxyTicketRequest object, which is why we need to call #proxy_ticket on it to retrieve the actual service ticket.

For extra security – and you will likely want to do this on production machines in the wild – in the proxied app’s configuration (some.other.appliction in this example) you can specify the list of authorized proxies. For example, on your proxied app the CAS configuration might look something like this:

CAS::Filter.cas_base_url = "https://login.example.com/cas"
CAS::Filter.server_name = "some.other.application"
CAS::Filter.authorized_proxies = ["https://yourrailsapp.com/cas/proxy_callback/receive_pgt"]

If no authorized proxies are given, the filter will accept receipts from any proxy.

Additional notes and caveats:

Note that when the CAS filter runs, the PGT is stored in session. This value must be passed to CAS::Filter#request_proxy_ticket. Also, note that CAS::Filter#request_proxy_ticket will URI-encode the service_uri before passing it to the CAS server, and the service value must henceforth always be passed as URI-encoded (this can be problematic when your proxied application uses some CAS client other than RubyCAS-Client).

The proxy url must be an https address. Otherwise CAS will refuse to communicate with it. This means that if you are using the bundled cas_proxy_callback controller, you will have to host your application on an https-enabled server. This can be a bit tricky with Rails. WEBrick’s SSL support is difficult to configure, and Mongrel doesn’t support SSL at all. One workaround is to use a reverse proxy like Pound, which will accept https connections and locally re-route them to your Rails application. Also, note that self-signed SSL certificates likely won’t work. You will probably need to use a real certificate purchased from a trusted CA authority (there are ways around this, but good luck :)

SSL Support

If you are getting an error on net/https – something like this:

no such file to load -- net/https

Then make sure the library for open SSL is installed. For example, on an Debian/Ubuntu system issue the following:

sudo apt-get install libopenssl-ruby

License

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program (see the file called LICENSE); if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA