Noninteractive login

This Wiki page documents Andrew's speculative coding of support for a CAS 3 login protocol.

Getting the speculative code

The speculative code is available as a Zip of a workspace.

Ideas from this code

Reverse engineering thoughts from this source code probably isn't the best use of your time. Writing the code was probably worthwhile because it did yield some ideas. I've extracted some of those ideas and documented them here:

AbstractUsernamePasswordHandler template method opportunity

The AbstractUsernamePasswordAuthenticationHandler object in the HEAD provides a template method authenticateInternal(Credentials). It will only call authenticateInternal(Credentials) for Credentials such that supports(Credentials) is true. AbstractUsernamePasswordHandler provides a final implementation of supports() that returns true where Credentials is not null and is an instanceof UsernamePasswordCredentials.

Discussed at video conference, and I see this has already been implemented in the HEAD:

AbstractUsernamePasswordAuthenticationHandler
/*
 * Copyright 2005 The JA-SIG Collaborative.  All rights reserved.
 * See license distributed with this file and
 * available online at http://www.uportal.org/license.html
 */
package org.jasig.cas.authentication.handler.support;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.cas.authentication.AuthenticationException;
import org.jasig.cas.authentication.principal.Credentials;
import org.jasig.cas.authentication.principal.UsernamePasswordCredentials;
import org.springframework.beans.factory.InitializingBean;

/**
 * Abstract class to override supports so that we don't need to duplicate the
 * check for UsernamePasswordCredentials.
 * 
 * @author Scott Battaglia
 * @version $Revision: 1.4 $ $Date: 2005/03/11 03:04:11 $
 * @since 3.0
 */
public abstract class AbstractUsernamePasswordAuthenticationHandler extends
    AbstractAuthenticationHandler implements InitializingBean {

    protected final Log log = LogFactory.getLog(getClass());

    protected final boolean authenticateInternal(final Credentials credentials)
        throws AuthenticationException {
        return authenticateUsernamePasswordInternal((UsernamePasswordCredentials)credentials);
    }

    protected abstract boolean authenticateUsernamePasswordInternal(
        UsernamePasswordCredentials credentials) throws AuthenticationException;

    protected final boolean supports(final Credentials credentials) {
        return credentials != null
            && UsernamePasswordCredentials.class.isAssignableFrom(credentials
                .getClass());
    }
}

Abstracting out what varies from the LoginController

There is logical workflow common across CAS deployments. The questions are the same. But the answers may be different.

By introducing the RequestToLoginConfig extension point to LoginController we can accomodate arbitrary per-institution rules about how we determine the service from a request, whether we support arbitrary services, how we determine whether the user needs to be warned that an authentication is occuring, how we determine whether we are in gateway mode, etc.

Suppose we invent the CAS3 protocol whereby a service conveys to /login what it's looking for in a service ticket. This would be implemented by a RequestToLoginConfig that knows how to tease this information out of the request and into a LoginConfig instance that knows how to answer these questions.

See notes from a Yale meeting.

ServiceTickets in the controller and view layers

The ServiceTicket object itself goes to the controller and view layers, where it is rendered. Ticket validations are views on tickets. The response from /login is also a view on tickets, where that view includes the TGT manifested as a cookie and the ST manifested as a redirect with a request parameter.

Increased flexibility of Proxy handling

The CAS 2 way

On a ticket validation, a service can request a proxy ticket. In CAS 2, the way we authenticate this requesting service is to send the (pgtid, pgtiou) pair to a proxy callback URL specified as a request parameter. This proxy callback URL must be over a secure channel. We verify its certificate. The ability to receive this callback authenticates the receiver. We then return in the ticket validation response the tgtiou. From the response, the service extracts the tgtiou and uses it to lookup the tgt from where it cached it.

CAS2 authenticates both the end user and the service to which a PGT is issued.

The WIND way

However, WIND does this differently. Instead of engaging in this callback, WIND simply includes the TGT directly in the ticket validation response when a ticket issued for authentication to services authorized to proxy is validated. For instance, when the Columbia uPortal instance validates the Service Ticket issued to it, CAS issues it a PGT which it parses from the ticket validation response.

Under WIND, the proxying service is not authenticated by secure callback. Since CAS offers its Login over SSL and since uPortal offers its Login over SSL, the Service Ticket is never exposed in the clear. As such, only the end user and the uPortal get the Service Ticket, and so a Ticket Granting Ticket in the name of the uPortal instance is potentially available to the end user as well as to the proxying service.

In order to accomodate this modification, the ProxyHandler API needs to be sufficiently flexible to return a PGTIOU (for CAS2 style responses) or to return the PGT itself (for WIND style responses).

Modeling this in code

ProxyHandler API
/**
 * Abstraction for what needs to be done to handle proxies. 
 * Useful because the generic flow for all authentication is 
 * similar the actions taken for
 * proxying are different. One can swap in/out implementations 
 * but keep the flow of events the same.
 * 
 * @author Scott Battaglia
 * @version $Id: ProxyHandler.java,v 1.1 2005/02/15 05:06:39 sbattaglia Exp $
 */
public interface ProxyHandler {

    /**
     * Given a list of Objects representing credentials that might authenticate a
     * service requesting a proxy granting ticket, and a String identifier for the
     * proxy granting ticket, generate an Object representing the result of the proxying
     * or throw an AuthenticationException indicating a reason why unable to 
     * generate proxying information.  
     * 
     * The proxying information returned by this method
     * is intended to be included as the proxy information property of the 
     * Authentication made available to the ticket validation response.
     * 
     * @param credentials The credentials of the service that will be proxying.
     * @param proxyGrantingTicketId The ticketId for the ProxyGrantingTicket (in CAS 3 this is a TicketGrantingTicket)
     * @return an object representing the result of proxying.
     * @throws AuthenticationException on failure
     */
    Object handle(List credentials, String proxyGrantingTicketId)
        throws AuthenticationException;
}

A CAS2-style handler considers a proxy callback URL to be potential credentials. It authenticates the credentials by executing the callback.

/**
 * Authenticate services by means of callback over HTTPS.
 * 
 * @author Scott Battaglia
 * @version $Id: Cas20ProxyHandler.java,v 1.3 2005/02/27 05:49:26 sbattaglia Exp $
 */
public class HttpsCallbackProxyHandler 
    extends AbstractProxyHandler implements InitializingBean {

    protected final Log log = LogFactory.getLog(getClass());

    private static final String PGTIOU_PREFIX = "PGTIOU";

    private UniqueTicketIdGenerator uniqueTicketIdGenerator;

    protected Object handleInternal(Object credentials, String proxyGrantingTicketId) {
        final HttpBasedServiceCredentials serviceCredentials = (HttpBasedServiceCredentials)credentials;
        final String proxyIou = this.uniqueTicketIdGenerator
            .getNewTicketId(PGTIOU_PREFIX);
        final StringBuffer stringBuffer = new StringBuffer();
        String response = null;

        stringBuffer.append(serviceCredentials.getCallbackUrl()
            .toExternalForm());

        if (serviceCredentials.getCallbackUrl().getQuery() == null)
            stringBuffer.append("?");
        else
            stringBuffer.append("&");

        stringBuffer.append("pgtIou=");
        stringBuffer.append(proxyIou);
        stringBuffer.append("&pgtId=");
        stringBuffer.append(proxyGrantingTicketId);

        try {
            response = UrlUtils.getResponseBodyFromUrl(new URL(stringBuffer
                .toString()));
        }
        catch (MalformedURLException e) {
            log.debug(e);
        }

        if (response == null) {
            log.info("Could not send ProxyIou of " + proxyIou
                + " for service: " + serviceCredentials.getCallbackUrl());
            return null;
        }
        log.info("Sent ProxyIou of " + proxyIou + " for service: "
            + serviceCredentials.getCallbackUrl());
        return new ProxyGrantingTicketIOU(proxyIou);

    }

    protected boolean supports(Object credential) {
        /*
         * We support non-null credentials of class Url credential.
         */
        return (credential != null 
                && HttpBasedServiceCredentials.class.isAssignableFrom(credential.getClass()));
    }
    
    /**
     * @param uniqueTicketIdGenerator The uniqueTicketIdGenerator to set.
     */
    public void setUniqueTicketIdGenerator(
        UniqueTicketIdGenerator uniqueTicketIdGenerator) {
        this.uniqueTicketIdGenerator = uniqueTicketIdGenerator;
    }

    public void afterPropertiesSet() throws Exception {
        if (this.uniqueTicketIdGenerator == null) {
            this.uniqueTicketIdGenerator = new DefaultUniqueTicketIdGenerator();
            log.info("No UniqueTicketIdGenerator specified for "
                + this.getClass().getName() + ".  Using "
                + this.uniqueTicketIdGenerator.getClass().getName());
        }
    }

}

What this example code does not currently do that it needs to do is to link its PGTIOU to the PGTID. Presumably it needs to place the PGTIOU as the key into a Map for which the PGTID is the value and this Map needs to also be accessible to the /Proxy CAS controller (or one of its dependencies).

A WIND ProxyHandler would compare take the value of "service" itself as a "credential". If the service registry indicates that we should be issuing a PGT to this service, a WIND ProxyHandler would simply return the PGTID as its Object return value.

A CAS3 ProxyHandler capable of authenticating services based upon, say, a client cert would consume client cert credentials and would, like WIND, return the PGTID directly.

Whatever it is that is returned by the ProxyHandler, the role of the ServiceValidateController is to place this object into the model available to the view. The deployer must configure views that know how to consume the output of the ProxyHandler.

    
List credentials = this.requestToCredentials.requestToCredentials(request);
            
// try to obtain a pgtIou if appropriate
if (! credentials.isEmpty()) {
  try {
                    
     final String proxyGrantingTicketId = this.centralAuthenticationService
             .delegateTicketGrantingTicket(serviceTicketId, credentials);

     if (proxyGrantingTicketId != null) {
         final Object proxyObject = this.proxyHandler.handle(credentials, proxyGrantingTicketId);
                        
         // put into the model the object returned from the proxy handler
         // which might be a ProxyGrantingTicketIou (default) or it might be a PGT string itself.
         // (plausible extension in the case where the requesting service iis authenticated
         // by some means other than https callback.
         model.put(WebConstants.PROXY_OBJECT, proxyObject);
     }
    

Richer AuthenticationHandler responses

The response from an AuthenticationHandler needs to be more than a boolean about whether the user was authenticated. It needs to be more than the Principal – who was authenticated and attributes of that user. It needs to be an Autentication, which potentially expresses both a Principal and a Set of AuthenticationEntries expressing how the Principal was authenticated.

In the case of authentication by binding with a username and password to an LDAP directory, where a user binding as himself can get additional attributes, we can take the opportunity at this Bind to produce an Authentication that expresses both "Authenticated awp9 via username and password" and "awp9 is Andrew Petro with these attributes."

In the cause of autentication via Shibboleth, again authentication and obtaining user attributes are intermingled since it is only after obtaining attributes that we know who it is that we have authenticated. A Shibboleth AuthenticationHandler would presumably return an Authentication modeling authentication via shibboleth and user attributes provided by Shibboleth.

In the cause of authentication against a Kerberos realm, we may have no attributes. The Authentication we return would contain a very simple, attribute-less principal which when the Authentications from all the AuthenticationHandlers are merged together would be replaced with a more fully-populated Principal via lookup against institutional data (LDAP, RDBMS, etc.) Or we might wire this lookup directly into our AuthenticationHandler so that it can return a fully populated Principal like its LDAP-bind and Shibboleth brethren.