RESTful API

New CAS documentation site

CAS documentation has moved over to apereo.github.io/cas, starting with CAS version 4.x. The wiki will no longer be maintained. For the most recent version of the documentation, please refer to the aforementioned link.

Applications are users?

Roughly, the useful intent of this capability is to model applications themselves as users, programmatically acquiring service tickets to authenticate to other applications, because those other applications found it expedient to use a CAS client library to accept Service Tickets rather than to rely upon some other technology for application-to-application authentication of requests (such as SSL certificates).

Of course, technically, this feature can be used to present end-user username and password pairs to CAS. There are some serious issues to consider in enabling that, not least of which is that naively implemented the REST endpoint becomes a tremendously convenient target for brute force dictionary attacks on your CAS server.  (Note that the threat of brute-force attacks can be somewhat mitigated by throttling login attempts in your underlying authentication mechanism.  Spring interceptor-based throttling (Throttling Login Attempts) is not applicable to restlets. -is this correct? )

Implement the RESTful CAS API only soberly and with due consideration of what you are doing. (That invitation to sobriety really applies across all things security and authentication.)

Purpose

Applications need to programmatically access CAS. Generally, proxying works for this. However, there are cases where an application needs to access a resource as itself, in which case proxying doesn't make any sense.

At Rutgers, we've implemented a relatively "heavyweight" SOAP based service via Axis. We're now looking at complementing that with a lightweight resource-driven architecture. This page details that proposed work.

This API works to expose a way to RESTfully obtain a Ticket Granting Ticket resource and then use that to obtain a Service Ticket.

Protocol

The RESTful API follows the same basic protocol as the original CAS2 protocol, augmented with some additional well-defined resource urls (though the protocol doesn't change so it should be just as secure).

Ticket Granting Ticket

The Ticket Granting Ticket is an exposed resource. It has a unique URI.

Request for a Ticket Granting Ticket Resource

POST /cas/v1/tickets HTTP/1.0

username=battags&password=password&additionalParam1=paramvalue

Response for a Ticket Granting Ticket Resource

Successful Response

201 Created
Location: http://www.whatever.com/cas/v1/tickets/{TGT id}

Unsuccessful Responses

If incorrect credentials are sent, CAS will respond with a 400 Bad Request error (will also respond for missing parameters, etc.). If you send a media type it does not understand, it will send the 415 Unsupported Media Type

Request for a Service Ticket

POST /cas/v1/tickets/{TGT id} HTTP/1.0

service={form encoded parameter for the service url}

Response for Service Ticket

Successful Response

200 OK

ST-1-FFDFHDSJKHSDFJKSDHFJKRUEYREWUIFSD2132

Unsuccessful Responses

If parameters are missing, etc. CAS will send a 400 Bad Request. If you send a media type it does not understand, it will send the 415 Unsupported Media Type.

Logout of the Service

To log out, you merely need to delete the ticket.

DELETE /cas/v1/tickets/TGT-fdsjfsdfjkalfewrihfdhfaie HTTP/1.0

Configuration

By default the CAS RESTful API is configured in the restlet-servlet.xml, which contains the routing for the tickets. It also defines the resources that will resolve the URLs. The TicketResource defined by default (which can be extended) accepts username/password.

To turn on the RESTful API, add the following to the web.xml:

<servlet>
	<servlet-name>restlet</servlet-name>
	<servlet-class>com.noelios.restlet.ext.spring.RestletFrameworkServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
	<servlet-name>restlet</servlet-name>
	<url-pattern>/v1/*</url-pattern>
</servlet-mapping>

Note, that in the above configuration example, we are explicitly versioning the RESTful API, so things would be accessed via /cas/v1/tickets/*, etc.

In the pom.xml file include the following:

<dependency>
	<groupId>org.jasig.cas</groupId>
	<artifactId>cas-server-integration-restlet</artifactId>
	<version>${cas.version}</version>
	<type>jar</type>
</dependency>

Please take note that there might be dependencies on Spring 2.x. Make sure to exclude them.

NOTE: In the 3.5.1  version these are the dependencies for integrating RESTful API cas-server-integration-restlet-3.5.1  (you can find them at this url: http://mvnrepository.com/artifact/org.jasig.cas/cas-server-integration-restlet/3.4.11-RC1):

  • cas-server-integration-restlet-3.5.1
  • cglib-nodep 2.1_3
  • com.noelios.restlet.ext.servlet 1.1.1
  • com.noelios.restlet.ext.spring 1.1.1
  • cas.server.core 3.5.1
  • org.restlet 1.1.1
  • org.restlet.ext.spring 1.1.1
  • spring-beans ${spring.version}

An issue caught while integrating all these jar in the the cas-server-webapp 3.5.1 is that the presence of cglib-full-2.0.2.jar ( deployed with cas-server-webapp 3.5.1.war) rises the following error on Tomcat server:

GRAVE: StandardWrapper.Throwable
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'root' defined in ServletContext resource [/WEB-INF/restlet-servlet.xml]: Cannot create inner bean 'org.restlet.ext.spring.SpringFinder#906704' of type [org.restlet.ext.spring.SpringFinder] while setting bean property 'attachments' with key [TypedStringValue: value [/tickets], target type [null]]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.restlet.ext.spring.SpringFinder#906704' defined in ServletContext resource [/WEB-INF/restlet-servlet.xml]: Instantiation of bean failed; nested exception is java.lang.StackOverflowError

Removing this jar from /WEB-INF/lib folder, solves the error.

Python REST Client Example

#!/usr/bin/python
import os.path
import httplib, urllib, urllib2, cookielib

# 1. Grab the Ticket Granting Ticket (TGT)


cas_host = "cas.acme.com"
rest_endpoint = "/cas/v1/tickets/"
params = urllib.urlencode({'username': 'battags', 'password': 'password'})
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", "User-Agent":"python" }
conn = httplib.HTTPSConnection(cas_host)
conn.request("POST", rest_endpoint, params, headers)
response = conn.getresponse()
print response.status, response.reason
data = response.read()
location = response.getheader('location')
#  Pull off the TGT from the end of the location, this works for CAS 3.3-FINAL
tgt = location[location.rfind('/') + 1:]
conn.close()

print location
print tgt
print "***"

# 2. Grab a service ticket (ST) for a CAS protected service

document = '/secure/blah.txt'
service  = 'http://docs.acme.com%s' % (document)

params = urllib.urlencode({'service': service })
conn = httplib.HTTPSConnection(cas_host)
conn.request("POST", "%s%s" % ( rest_endpoint, tgt ), params, headers)
response = conn.getresponse()
print response.status, response.reason
st = response.read()
conn.close()

print "service: %s" % (service)
print "st     : %s" % (st)
print "***"

# 3. Grab the protected document

url  = "%s?ticket=%s" % ( service, st )  # Use &ticket if service already has query parameters
print "url    : %s" % (url)

cj = cookielib.CookieJar()

# no proxies please
no_proxy_support = urllib2.ProxyHandler({})
# we need to handle session cookies AND redirects
cookie_handler = urllib2.HTTPCookieProcessor(cj)

opener = urllib2.build_opener(no_proxy_support, cookie_handler, urllib2.HTTPHandler(debuglevel=1))
urllib2.install_opener(opener)
protected_data = urllib2.urlopen(url).read()
print protected_data

Python REST Client Example - Spring Security Server

#!/usr/bin/python
import os.path
import httplib, urllib, urllib2, cookielib

# Spring security protected urls do not consume the service ticket if it is present on the document url, so a slightly different approach is required

# 1. Grab the Ticket Granting Ticket (TGT)

params = urllib.urlencode({'username': 'enter-it-here', 'password': 'enter-it-here'})
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
conn = httplib.HTTPSConnection("cas.acme.com")
conn.request("POST", "/cas/v1/tickets/", params, headers)
response = conn.getresponse()
print response.status, response.reason
data = response.read()
location = response.getheader('location')
# Pull off the TGT from the end of the location, this works for CAS 3.4.11
tgt = location[location.rfind('/') + 1:]
conn.close()
 
print location
print tgt
print "--------------"
 
# 2. Grab a service ticket (ST) for a CAS protected service which directs to a service ticket consumption page. Spring Security does this, for example.
document = 'http://docs.acme.com/secure/blah.txt'

# Spring security default service ticket consumption page
service = 'http://docs.acme.com/secure/j_spring_cas_security_check'

params = urllib.urlencode({'service': service })
conn = httplib.HTTPSConnection("cas.acme.com")
conn.request("POST", "/cas/v1/tickets/%s" % ( tgt ), params, headers)
response = conn.getresponse()
print response.status, response.reason
st = response.read()
conn.close()
 
print "service: %s" % (service)
print "st : %s" % (st)
print "--------------"
 
url = "%s?ticket=%s" % ( service, st ) # Use &ticket if service already has query parameters
print "url : %s" % (url)
 
cj = cookielib.CookieJar()
 
# no proxies please
no_proxy_support = urllib2.ProxyHandler({})
# we need to handle session cookies AND redirects
cookie_handler = urllib2.HTTPCookieProcessor(cj)
 
opener = urllib2.build_opener(no_proxy_support, cookie_handler, urllib2.HTTPHandler(debuglevel=1))
urllib2.install_opener(opener)
print "Establishing application session via service ticket consumption url..."
st_response = urllib2.urlopen(url).read()

# 3. Now we can grab the protected document

print "Retrieving document..."
protected_data = urllib2.urlopen(document).read()
print protected_data[:100]

 

Java REST Client Example

We need a real, working, example, the previous one is useless. Many people are emailing me that it is not working, and I confirm it does not work.

Bash Example

# This file is used to store the Ticket Getting Ticket
rm tgt.txt

# This file is used to store the Service Ticket
rm serviceTicket.txt

#This file is used to store the service call response
rm response.txt

export CAS_LOGIN_URL=http://localhost:8080/cas/v1/tickets
export GET_URL=http://localhost:8080/service
export USERNAME=username
export PASSWORD=password

# Request a new Ticket Getting Ticket (TGT).  This returns HTML which is put into tgt.txt.
wget --no-check-certificate -O tgt.txt --post-data="username=$USERNAME&password=$PASSWORD" $CAS_LOGIN_URL

# Extract from the HTML the TGT and put back into tgt.txt
echo TGT`grep -oEi 'action=\".*\"' tgt.txt | grep -oEi '\-.*\-cas'` > tgt.txt

# display the TGT
cat tgt.txt

# Request a new Service Ticket and store in serviceTicket.txt
wget --no-check-certificate --post-data="service=$GET_URL" -O serviceTicket.txt $CAS_LOGIN_URL/`cat tgt.txt`

# Get the data at from the service at GET_URL and store in response.txt
wget --no-check-certificate -O response.txt $GET_URL?ticket=`cat serviceTicket.txt`

# Display the data from the service call
cat response.txt

Groovy Example

@Grab("commons-httpclient:commons-httpclient:3.1")
import java.io.IOException
import java.util.logging.Logger
import java.util.regex.Matcher
import java.util.regex.Pattern
import org.apache.commons.httpclient.HttpClient
import org.apache.commons.httpclient.NameValuePair
import org.apache.commons.httpclient.methods.PostMethod
import org.apache.commons.httpclient.methods.GetMethod
import org.apache.commons.httpclient.methods.DeleteMethod
class Client
{
  static final Logger LOG = Logger.getLogger(Client.class.getName())
  String getServiceTicket(String server, String ticketGrantingTicket, String service)
  {
    if (!ticketGrantingTicket)
      return null
    HttpClient client = new HttpClient()
    PostMethod post = new PostMethod("$server/$ticketGrantingTicket")
    post.setRequestBody([new NameValuePair("service", service)].toArray(new NameValuePair[1]))
    try
    {
      client.executeMethod(post)
      String response = post.getResponseBodyAsString()
      switch (post.getStatusCode())
      {
        case 200:
          return response
        default:
          LOG.warning("Invalid response code ( $post.getStatusCode() ) from CAS server!")
          LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())))
          break
      }
    }
    catch (final IOException e)
    {
      LOG.warning(e.getMessage())
    }
    finally
    {
      post.releaseConnection()
    }
    return null
  }
  String getTicketGrantingTicket(String server, String username, String password)
  {
    HttpClient client = new HttpClient()
    PostMethod post = new PostMethod(server)
    post.setRequestBody([new NameValuePair("username", username),new NameValuePair("password", password)].toArray(new NameValuePair[2]))
    try
    {
      client.executeMethod(post)
      String response = post.getResponseBodyAsString()
      switch (post.getStatusCode())
      {
        case 201:
          Matcher matcher = Pattern.compile(".*action=\".*/(.*?)\".*").matcher(response)
          if (matcher.matches())
            return matcher.group(1)
          LOG.warning("Successful ticket granting request, but no ticket found!")
          LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())))
          break
        default:
          LOG.warning("Invalid response code ($post.getStatusCode()) from CAS server!")
          LOG.info("Response: $response")
          break
      }
    }
    catch (final IOException e)
    {
      LOG.warning(e.getMessage())
    }
    finally
    {
      post.releaseConnection()
    }
    return null
  }
  void notNull(Object object, String message)
  {
    if (object == null)
      throw new IllegalArgumentException(message)
  }
  void getServiceCall(String service, String serviceTicket) {
      HttpClient client = new HttpClient()
      GetMethod method = new GetMethod(service)
      method.setQueryString([new NameValuePair("ticket", serviceTicket)].toArray(new NameValuePair[1]))
      try
      {
        client.executeMethod(method)
        String response = method.getResponseBodyAsString()
        switch (method.getStatusCode())
        {
          case 200:
            LOG.info("Response: $response")
            break
          default:
            LOG.warning("Invalid response code (" + method.getStatusCode() + ") from CAS server!")
            LOG.info("Response: $response")
            break
        }
      }
      catch (final IOException e)
      {
        LOG.warning(e.getMessage())
      }
      finally
      {
          method.releaseConnection()
      }
  }
  void logout(String server, String ticketGrantingTicket) {
      HttpClient client = new HttpClient()
      DeleteMethod method = new DeleteMethod("$server/$ticketGrantingTicket")
      try
      {
        client.executeMethod(method)
        switch (method.getStatusCode())
        {
          case 200:
            LOG.info("Logged out")
            break
          default:
            LOG.warning("Invalid response code (" + method.getStatusCode() + ") from CAS server!")
            LOG.info("Response: $response")
            break
        }
      }
      catch (final IOException e)
      {
        LOG.warning(e.getMessage())
      }
      finally
      {
          method.releaseConnection()
      }
  }
  public static void main(String[] args)
  {
        String server = "http://localhost:8080/cas/v1/tickets"
        String username = "username"
        String password = "password"
        String service = "http://localhost:8080/service"
        Client client = new Client()
        String ticketGrantingTicket = client.getTicketGrantingTicket(server, username, password)
        println "TicketGrantingTicket is $ticketGrantingTicket"
        String serviceTicket = client.getServiceTicket(server, ticketGrantingTicket, service)
        println "ServiceTicket is $serviceTicket"
        client.getServiceCall(service, serviceTicket)
        client.logout(server, ticketGrantingTicket)
  }
}