CAS and JSR-168

This page is for documenting how to CASify JSR-168 portlets, particularly how to allow them to obtain proxy tickets.

Big picture

As of uPortal 2.5, you can override methods in CPortletAdapter to set PortletRequest attributes.

Suppose we do so. We invent a service String representing our portlet. We use an IYaleCasContext LocalConnectionContext implementation to obtain a proxy ticket for this service (in uPortal). We place this into a portlet request attribute on first invocation of our portlet.

The portlet validates the proxy ticket we passed as a PortletRequest attribute, specifying a proxy callback URL of a mapped instance of the ProxyTicketReceptor servlet in the portlet webapplication. CAS sends the portlet.war a proxy granting ticket of its own (chained off of the uportal PGT, but not the uPortal PGT. Services to which the portlet proxies can identify the particular .war instance that is proxying, rather than just identifying the portal generally. Particular back end services might trust the grades portlet to proxy to more than just the resources to which the bookmarks portlet may proxy.)

Implementation

Code sketches.

A wrapper for a portlet, implementing establishing the CASReceipt into the PortletSession.

CASPortletWrapper
package edu.yale.its.tp.cas.client.portlet;

import java.io.IOException;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.Portlet;
import javax.portlet.PortletConfig;
import javax.portlet.PortletException;
import javax.portlet.PortletRequest;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import edu.yale.its.tp.cas.client.CASAuthenticationException;
import edu.yale.its.tp.cas.client.ProxyTicketValidatorFactory;

/**
 * A JSR-168 portlet wrapper that validates a CAS ticket present as a 
 * particular portlet request parameter.
 * @version $Revision:$ $Date:$
 */
public class CASPortletWrapper 
    implements Portlet {
    
    public static String WRAPPED_PORTLET_CLASS = "edu.yale.its.tp.cas.client.portlet.WRAPPED_PORTLET_CLASS";
    
    /**
     * The Portlet we're wrapping and to which we delegate.
     */
    private Portlet delegate;

    private ProxyTicketValidatorFactory ptvFactory;
    
    public void init(PortletConfig pc) throws PortletException {
        // extract our own configuration from the config and then
        // pass the config along to our delegate.
        
        String wrappedPortlet = pc.getInitParameter(WRAPPED_PORTLET_CLASS);
        if (wrappedPortlet == null) {
            throw new PortletException("CASPortlet requires the init parameter WRAPPED_PORTLET_CLASS");
        }
        
        try {
            this.delegate = (Portlet) Class.forName(wrappedPortlet).newInstance();
        } catch (Exception e) {
            throw new PortletException("Could not intantiate delegate class " + delegate, e);
        }
        
        this.ptvFactory = CASPortletUtils.ptvFactoryForPortletConfig(pc);
        
        this.delegate.init(pc);
        
    }

    public void processAction(ActionRequest actionRequest, ActionResponse actionResponse) throws PortletException, IOException {
        try {
            CASPortletUtils.establishSession(actionRequest, this.ptvFactory);
        } catch (CASAuthenticationException e) {
            throw new PortletException(e);
        }
        
        this.delegate.processAction(actionRequest, actionResponse);
    }

    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws PortletException, IOException {
        try {
            CASPortletUtils.establishSession(renderRequest, this.ptvFactory);
        } catch (CASAuthenticationException e) {
            throw new PortletException(e);
        }
        
        this.delegate.render(renderRequest, renderResponse);
    }

    public void destroy() {
        this.delegate.destroy();
    }

}

This relies heavily on a helper class (which other portlets can use without using the wrapper portlet):

CASPortletUtils
package edu.yale.its.tp.cas.client.portlet;

import java.io.IOException;

import javax.portlet.PortletConfig;
import javax.portlet.PortletRequest;
import javax.portlet.PortletSession;

import edu.yale.its.tp.cas.client.CASAuthenticationException;
import edu.yale.its.tp.cas.client.CASReceipt;
import edu.yale.its.tp.cas.client.ProxyTicketValidator;
import edu.yale.its.tp.cas.client.ProxyTicketValidatorFactory;
import edu.yale.its.tp.cas.client.filter.CASFilter;
import edu.yale.its.tp.cas.proxy.ProxyTicketReceptor;

/**
 * Helper class for CASifying portlets.
 * @version $Revision:$ $Date:$
 */
public class CASPortletUtils {

    /**
     * Name of the Portlet initialization parameter that should have as its value the 
     * "service" string representing the portlet.
     */
    public static final String PORTLET_SERVICE_URL_PARAM = 
        "edu.yale.its.tp.cas.client.portlet.CasPortletUtilsPORTLET_SERVICE";
    
    /**
     * Name of the Portlet initialization parameter that should have as its value the 
     * https URL whereat this portlet webapplication is receiving its proxy granting tickets.
     */
    public static final String PROXY_CALLBACK_URL_PARAM = 
        "edu.yale.its.tp.cas.client.portlet.CasPortletUtils.PROXY_CALLBACK_URL";
    
    
    /**
     * Name of the Portlet initialization parameter that should have as its value the 
     * https URL whereat we should validate our tickets.
     */
    public static final String CAS_VALIDATE_URL_PARAM = 
        "edu.yale.its.tp.cas.client.portlet.CasPortletUtils.CAS_VALIDATE_URL";
    
    /**
     * Name of the PortletRequest attribute that should have as its value a
     * proxy ticket to the Service keying this portlet.
     */
    public static final String TICKET_ATTRIBUTE =
        "edu.yale.its.tp.cas.client.portlet.CasPortletUtils.TICKET";
    
    /**
     * Get a ProxyTicketValidatorFactory instance configured according to the 
     * portlet initialization parameters in the PortletConfig.
     * @param pc
     * @return
     */
    public static ProxyTicketValidatorFactory ptvFactoryForPortletConfig(PortletConfig pc) {
        
        ProxyTicketValidatorFactory factory = new ProxyTicketValidatorFactory();
        
        String service = pc.getInitParameter(PORTLET_SERVICE_URL_PARAM);
        if (service == null) {
            throw new IllegalArgumentException("Required portlet init param " + PORTLET_SERVICE_URL_PARAM + " not declared.");
        }
        factory.setService(service);
        
        String proxyCallbackUrl = pc.getInitParameter(PROXY_CALLBACK_URL_PARAM);
        if (proxyCallbackUrl == null) {
            throw new IllegalArgumentException("Required portlet init param " + PROXY_CALLBACK_URL_PARAM + " not declared.");
        }
        factory.setProxyCallbackUrl(proxyCallbackUrl);
        
        String casValidateUrl = pc.getInitParameter(CAS_VALIDATE_URL_PARAM);
        if (casValidateUrl == null) {
            throw new IllegalArgumentException("Required portlet init param " + CAS_VALIDATE_URL_PARAM + " not declared.");
        }
        factory.setCasValidateUrl(casValidateUrl);
        
        return factory;
    }
    
    /**
     * Ensure that a portlet session has a CASReceipt stored into it reflecting the
     * authenticated user.  CASReceipts convey the PGT_IOU whereby a portlet (or other
     * web component) can lookup the Proxy Granting Ticket when can acquire a Proxy Ticket
     * to access some particular service.
     * @param request
     * @throws CASAuthenticationException on failure to validate a ticket
     */
    public static void establishSession(PortletRequest request, ProxyTicketValidatorFactory factory) throws CASAuthenticationException {
        PortletSession session = request.getPortletSession();
        
        CASReceipt receipt = (CASReceipt) session.getAttribute(CASFilter.CAS_FILTER_RECEIPT);
        
        if (receipt == null) {
            ProxyTicketValidator validator = factory.buildTicketValidator();
            String ticket = (String) request.getAttribute(TICKET_ATTRIBUTE);
            validator.setServiceTicket(ticket);
            CASReceipt.getReceipt(validator);
            session.setAttribute(CASFilter.CAS_FILTER_RECEIPT, receipt);
        }
        
    }
    
    
    /**
     * Get a ProxyTicket to authenticate to a particular service, given a request 
     * associated with a session into which a CASReceipt has been stored.
     * @param request
     * @param targetService
     * @return
     */
    public static String getProxyTicket(PortletRequest request, String targetService) {
        CASReceipt receipt = (CASReceipt) request.getPortletSession().getAttribute(CASFilter.CAS_FILTER_RECEIPT);
        
        String pgtIou = receipt.getPgtIou();
        
        try {
            return ProxyTicketReceptor.getProxyTicket(pgtIou, targetService);
        } catch (IOException e) {
            throw new RuntimeException("Could not obtain proxy ticket for service " + targetService, e);
        }
        
    }
    
}

This helper class in turn relies upon a new factory for ProxyTicketValidator instances:

ProxyTicketValidatorFactory
package edu.yale.its.tp.cas.client;

/**
 * Produces configured ProxyTicketValidators.  The ProxyTicketValidators produced
 * by this factory may be further configured (must at least be further configured
 * to know about the ticket to be validated.)
 * @version $Revision:$ $Date:$
 */
public class ProxyTicketValidatorFactory {

    /**
     * The URL whereat CAS server offers its service and proxy ticket validation
     * service. The default value for the casValidateUrl property of the ProxyTicketValidator
     * instances we produce.
     */
    private String casValidateUrl;
    
    /**
     * The HTTPS URL whereat the code associated with this factory
     * receives proxy granting tickets.  The default value for the proxyCallbackUrl
     * property of the ProxyTicketValidators we produce.
     */
    private String proxyCallbackUrl;

    /**
     * True if we should configure the ProxyTicketValidator instances we produce
     * to only validate service tickets derived directly from a presentation of primary
     * credentials.  False otherwise.  Defaults to false.  More specifically, the default
     * value for the renew property of the ProxyTicketValidators we produce.
     */
    private boolean renew = false;

    /**
     * The service we expect to be associated with tickets the ProxyTicketValidators
     * we produce will be validating.  The default value for the service property of
     * the ProxyTicketValidators we produce.
     */
    private String service;
    
    /**
     * Returns a new ProxyTicketValidator configured according to the state
     * of this factory.
     * @return
     */
    public ProxyTicketValidator buildTicketValidator(){
        ProxyTicketValidator ptv = new ProxyTicketValidator();
        ptv.setCasValidateUrl(this.casValidateUrl);
        ptv.setProxyCallbackUrl(this.proxyCallbackUrl);
        ptv.setRenew(this.renew);
        ptv.setService(this.service);
        return ptv;
    }
    
    /**
     * Get the default value for the casValidateUrl property of the ProxyTicketValidators
     * we produce.
     * @return Returns the casValidateUrl.
     */
    public String getCasValidateUrl() {
        return this.casValidateUrl;
    }
    
    /**
     * Set the default value for the casValdateUrl property of the ProxyTicketValidators
     * we produce.
     * @param casValidateUrl The casValidateUrl to set.
     */
    public void setCasValidateUrl(String casValidateUrl) {
        
        // TODO: check that the casValidateUrl is an Https URL.
        
        this.casValidateUrl = casValidateUrl;
    }
    
    /**
     * Get the default value for the proxyCallbackUrl property of the ProxyTicketValidators
     * we produce.
     * @return Returns the proxyCallbackUrl.
     */
    public String getProxyCallbackUrl() {
        return this.proxyCallbackUrl;
    }
    
    /**
     * Set the default value for the proxyCallbackUrl property of the ProxyTicketValidators
     * we produce.
     * @param proxyCallbackUrl The proxyCallbackUrl to set.
     */
    public void setProxyCallbackUrl(String proxyCallbackUrl) {
        
        // TODO: check that this URL is an https URL.
        
        this.proxyCallbackUrl = proxyCallbackUrl;
    }
    
    /**
     * Get the default value for the renew property of the ProxyTicketValidator
     * instances that we produce.
     * @return Returns the renew.
     */
    public boolean isRenew() {
        return this.renew;
    }
    
    /**
     * Set the default value for the renew property of ProxyTicketValidator 
     * instances that we produce.
     * @param renew The renew to set.
     */
    public void setRenew(boolean renew) {
        this.renew = renew;
    }
    
    /**
     * Get the default value for the service property of ProxyTicketValidator 
     * instances that we produce.
     * @return Returns the service.
     */
    public String getService() {
        return this.service;
    }
    
    /**
     * Set the default value for the service property of ProxyTicketValidator 
     * instances that we produce.
     * @param service The service to set.
     */
    public void setService(String service) {
        this.service = service;
    }



    
}