Portlets using Proxy CAS

This page is for design and discussion of how to make JSR-168 Portlets use Proxy CAS.

Contents out of date

This page should be updated to include discussion of uPortal 3.x UserInfoService-based CAS Proxy strategy

Why use Proxy CAS

The short version of the proxy CAS value proposition is that it allows authentication of a chain of services (say, your Email Portlet as accessed from your uPortal instance) and its participation in a Single Sign On session (say, end user "awp9" authenticated via CAS to your uPortal, which proxy authenticated via CAS to your JSR-168 email portlet, which proxy authenticated via CAS to your IMAP server). All of this happens with the end user authenticated to CAS using, say, a username and password and the services authenticated to CAS via server-side SSL certificates. No forwarding of primary credentials - only CAS server gets to see the password.

How proxy CAS works for uPortal channels

Use CAS for login to your portal. At the time the portal validates the service ticket presented to it, it requests a proxy granting ticket. CAS calls back to uPortal's SSL'ed servlet to deposit the proxy granting ticket, thereby authenticating the uPortal instance.

This Proxy Granting Ticket identifier is available from the IYaleCasContext security context. Channels use a CasConnectionContext (a subclass of the generic LocalConnectionContext API) to place proxy tickets onto URLs they'd like to use. (Specifically, CasConnectionContext calls the IYaleCasContext to use the proxy granting ticket to obtain a proxy ticket for the URL and append it to the URL).

Anyway, channels use CasConnectionContext to proxy to backing sources.

Suppose you write an IChannel that speaks CASified IMAP: then the proxy chain is:
uPortal --> IMAP server.

Architecture: how Proxy CAS can work for JSR-168 portlets

Option 1: Portlets are like channels

Just like was the case with CASified IChannels, the proxy chain is:
uPortal --> IMAP server.

Just like with IChannels, the uPortal at user authentication to the portal via CAS requests and obtains a Proxy Granting Ticket in the name of the portal itself.

Modify (or subclass) CPortletAdapter to pass the Proxy Granting Ticket identifier to the JSR-168 portlet.

The JSR-168 can then use the PGT to acquire proxy tickets in the name of the uPortal, proxying the end user's authentication. Very similar to the way IChannels do this, except we're not using the LocalConnectionContext and instead we're doing something more CAS-specific.

Option 2: Doing better than channels

There's a tremendous opportunity here to do "better" with Proxy CAS in JSR-168 portlets than we did with proxy CAS in IChannels. This opportunity is borne of the modularity of JSR-168 portlets and their availability as web applications.

The uPortal obtains a Proxy Granting Ticket in the name of the uPortal instance at the time the user authenticates.

We modify (or subclass) CPortletAdapter to use the Proxy Granting Ticket to obtain a Proxy Ticket addressed to a dummy service URL agreed to by portal and portlet. Say, something like http://my.someschool.edu/portletCasServiceTargets/portletApp.portlet , where portletApp.portlet identifies the particular portlet. Nothing's actually listening at this URL, it's just a convention.

CPortletAdapter passes the proxy ticket to the JSR-168. The JSR-168 validates the proxy ticket and when it does so it requests a Proxy Granting Ticket be issued. CAS does an SSL callback to authenticate the portlet. Since the portlet is a real web application, it can map a listener to receive the proxy granting ticket.

Then the portlet uses that proxy granting ticket to acquire proxy tickets to proxy to back end services.

What we've gained here is we've included the particular authenticated portlet in the proxy chain, such that the ultimate target of these proxy tickets can verify not just what user's session we're part of, not just that it's your uPortal asking, but which portlet it is that's asking. An HR portlet is differentiable from the email portlet, such that the email portlet couldn't proxy into your payroll system and the HR portlet couldn't proxy in and get my email. More specific, better granularity of authentication. Something we didn't have with IChannels.

Implementation

Suppose we want to use option 2.

Subclassing CPorletAdapter

We can subclass CPortletAdapter to produce a version of CPortletAdapter that acquires and supplies a proxy ticket without imposing this functionality on every portlet. JSR-168s taking advantage of our CAS-specific usages can work in uPortal via the more specific CPortletAdapter subclass.

So let's subclass CPortletAdapter to produce CasPortletAdapter.

Subclassing lets us override two things. One is the user attributes presented by portal to portlet. Let's leave those alone and accept the default CPortletAdapter behavior for those. Another is request attribute generation. Let's override that to add a request attribute in which to pass the String that is the proxy ticket and another attribute to pass the String that is the "service" identifier required for proxy ticket validation. Our CasPortletAdapter will need to take an additional channel static data parameter configuring that "service" identifier.

Okay. So CasPortletAdapter reads a channel static data parameter to know the "service" identifier, accesses the IYaleCasContext security context to acquire a proxy ticket for that service identifier, and includes both the service identifier and the proxy ticket as Portlet request attributes.

Making our JSR-168 CAS-aware

The JSR-168 needs to read the portlet request parameters conveying the proxy ticket and the service identifier. It then needs to use ProxyTicketValidator from the CAS Java Client jar to validate the proxy ticket, specifying a proxy callback URL that is mapped in web.xml to an instance of the CAS Java Client ProxyTicketReceptor servlet. The proxy callback URL should probably be configured in portlet.xml as a parameter to our portlet.

The portlet invokes the ProxyTicketValidator to validate the ticket and then uses it to access the PGTIOU. It can then use the PGTIOU to access the ProxyTicketReceptor and thereby acquire proxy tickets to access the resources to which it would like to proxy.

It should store the pgtiou, or perhaps a CASReceipt, into the Portlet session so that it won't need to receive and validate a proxy ticket on every request from portal to portlet.

Is JSR-168 compliant?

This introduces CAS-specific extensions within the framework of JSR-168: specifically, CAS-specific Portlet request attributes. A JSR-168 written to take advantage of CAS proxy functionality using this approach will require its portlet container to provide these CAS-specific request attributes. This means that the portlet won't work in a random portlet container that doesn't know how to provide these CAS-specific portlet attributes.

Code sketches

Related thoughts

If you squint hard enough, a Sakai Tool in Sakai looks a whole lot like a JSR-168 portlet in uPortal. These Powerpoint slides attached to this Wiki page include notes about how such tools might take full advantage of proxy CAS.

The ESUP-Portail implementation

Pass a Proxy Ticket to portlets 

The ESUP-Portail project uses a subclass of CPortletAdaptor called CCasProxyPortletAdaptor (CCasProxyPortletAdapter.java). This class passes a Proxy Ticket through JSR-168 preferences to the portlets.

In order to be CAS-aware, the portlet must be published with this new adaptor, for instance:

<channel-definition>
    ...
    <type>CasProxyPortlet</type>
    <class>[...].channels.portlet.CCasProxyPortletAdapter</class>
    ...
    <parameters>
        <parameter>
            <name>portletDefinitionId</name>
            <value>esup-blank.esup-blank</value>
            <ovrd>N</ovrd>
        </parameter>
        <parameter>
            <name>casProxyTicketPref</name>
            <value>casProxyTicket</value>
            <description>
              The name of the JSR-168 preference used to
              pass the proxy ticket. Optional, defaults to 'casProxyTicket'
            </description>
            <ovrd>N</ovrd>
        </parameter>
        <parameter>
            <name>casTargetService</name>
            <value>http://portal.domain.edu/application</value>
            <description>The CAS service of the portlet</description>
            <ovrd>N</ovrd>
        </parameter>
    </parameters>
</channel-definition>

Once published this way, the portlet is automatically an additional JSR-168 preference named casProxyTicket that can be validated by the CAS server to get a PGT. The PGT can later retrieve Proxy Tickets for backend services. Parameter casTargetService is a dummy portal URL, the portlet should use the same URL when validating the ticket.

Note: no PT is passed to the portlet if parameter casTargetService is not set, so the adaptor can be used for any portlet.

Get a Proxy Ticket from the portlet for a backend service

Proxy Tickets for backend services are retrieved by calling a bean (casService):

String casTargetService = "http://service.domain.edu/path";
String pt = null;
try {
    pt = casService.getProxyTicket(casTargetService);
} catch (CasException e) {
    ...
}

The bean casService is defined like this:

<bean
    id="casService"
    class="[...].commons.services.cas.PortletCasServiceImpl" >
    <property
      name="service"
      value="http://portal.domain.edu/application" />
    <property
      name="casValidateUrl"
      value="https://cas.domain.edu/proxyValidate" />
    <property
     name="proxyCallbackUrl"
     value="https://portal.domain.edu/application/casProxyCallback" />
    <property
     name="casProxyTicketPref"
     value="casProxyTicket" />
</bean>

The bean casService automatically validates the Proxy Ticket passed by the portal the first time getProxyTicket() is called. It is possible to anticipate the validation by manually calling method validate() (to prevent from PT expiration if the portlet is not rendered soon enough).

Of course, the tomcat context of the portlet must define the callback URL:

<servlet>
    <servlet-name>CasProxyCallback</servlet-name>
    <servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class>
    <init-param>
      <param-name>edu.yale.its.tp.cas.proxyUrl</param-name>
      <param-value>https://cas.domain.edu/proxy</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>CasProxyCallback</servlet-name>
    <url-pattern>/casProxyCallback</url-pattern>
</servlet-mapping>