Standalone Java CAS Client
There's a variety of clients for CAS. The Java-based clients (JA-SIG, Yale, see JA-SIG website) typically handle the browser-based client interaction with CAS very well through ServletFilter implementations.Now what about programmatic authentication, i.e. achieving authentication through non-browser based applications? There exists a CAS .NET client but I did not manage to find the appropriate Java implementation. So here goes - it is based on the Apache HttpClient.
In case I missed any existing implementation achieving the same purpose, let's look at the bright side: at least now I understand the CAS protocol :-)
My CAS client works within any application. It uses the HttpClient and behaves like a browser client as CAS requires cookie support.
Here's the code:
import
org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
/**
* The CasClient allows users to programmatically login
* to CAS protected services based on the CAS 2 protocol.
* This client behaves like a browser-client in terms of
* cookie handling.<br>
*
* @author Mathias Richter
*/
public class CasClient
{
public static Logger LOG = Logger.getLogger( CasClient. class );
public static final String LOGIN_URL_PART = " login " ;
public static final String SERVICE_VALIDATE_URL_PART = " serviceValidate " ;
public static final String TICKET_BEGIN = " ticket= " ;
private static final String LT_BEGIN = " name= " lt " value= "" ;
public static final String CAS_USER_BEGIN = " <cas:user> " ;
public static final String CAS_USER_END = " </cas:user> " ;
private HttpClient fClient;
private String fCasUrl;
/**
* Construct a new CasClient.
*
* @param casUrl The base URL of the CAS service to be used.
*/
public CasClient( String casBaseUrl )
{
this ( new HttpClient(), casBaseUrl );
}
/**
* Construct a new CasClient which uses the specified HttpClient
* for its HTTP calls.
*
* @param client
* @param casBaseUrl
*/
public CasClient( HttpClient client, String casBaseUrl )
{
fClient = client;
fCasUrl = casBaseUrl;
}
/**
* Authenticate the specified username with the specified password.
* This will not yield any ticket, as no service is authenticated
* against. This wil just set the CAS cookie in this client upon
* successful authentication.
*
* @param username
* @param password
*/
public void authenticate( String username, String password )
{
authenticate( null , username, password );
}
/**
* Validate the specified service ticket against the specified service.
* If the ticket is valid, this will yield the clear text user name
* of the autenticated user.<br>
* Note that each service ticket issued by CAS can be used exactly once
* to validate.
*
* @param serviceUrl
* @param serviceTicket
*
* @return Clear text username of the authenticated user.
*/
public String validate( String serviceUrl, String serviceTicket )
{
String result = null ;
PostMethod method = new PostMethod( fCasUrl + SERVICE_VALIDATE_URL_PART );
method.setParameter( " service " , serviceUrl );
method.setParameter( " ticket " , serviceTicket );
try
{
int statusCode = fClient.executeMethod(method);
if (statusCode != HttpStatus.SC_OK)
{
LOG.error( " Could not validate: " + method.getStatusLine() );
method.releaseConnection();
} else
{
result = extractUser( new String( method.getResponseBody() ) );
}
} catch ( Exception x )
{
LOG.error( " Could not validate: " + x.toString () );
x.printStackTrace();
}
method.releaseConnection();
return result;
}
/**
* Authenticate the specified user with the specified password against the
* specified service.
*
* @param serviceUrl May be null. If a url is specified, the authentication will happen against this service, yielding a service ticket which can be validated.
* @param username
* @param password
* @return A valid service ticket, if and only if the specified service URL is not null.
*/
public String authenticate( String serviceUrl, String username, String password )
{
String lt = getLt( serviceUrl );
if ( lt == null )
{
LOG.error( " Cannot retrieve LT from CAS. Aborting authentication for ' " + username + " ' " );
return null ;
}
String result = null ;
PostMethod method = new PostMethod( fCasUrl + LOGIN_URL_PART );
if ( serviceUrl != null ) // optional
method.setParameter( " service " , serviceUrl );
method.setParameter( " _eventId " , " submit " );
method.setParameter( " username " , username );
method.setParameter( " password " , password );
method.setParameter( " lt " , lt );
method.setParameter( " gateway " , " true " );
try
{
fClient.executeMethod(method);
if ( serviceUrl == null )
{
if ( extractLt( new String( method.getResponseBody() ) ) != null ) // if CAS does not return a login page with an LT authentication was successful
{
LOG.error( " Authentication for ' " + username + " ' unsuccessful " );
if ( LOG.isDebugEnabled() )
LOG.debug( " Authentication for ' " + username + " ' unsuccessful. " );
} else
{
if ( LOG.isDebugEnabled() )
LOG.debug( " Authentication for ' " + username + " ' unsuccessful. " );
}
} else
{
Header h = method.getResponseHeader( " Location " );
if ( h != null )
result = extractServiceTicket( h.getValue() );
if ( result == null )
LOG.error( " Authentication for ' " + username + " ' unsuccessful. " );
}
} catch ( Exception x )
{
LOG.error( " Could not authenticate' " + username + " ': " + x.toString () );
}
method.releaseConnection();
return result;
}
/**
* Helper method to extract the user name from a "service validate" call to CAS.
*
* @param data Response data.
* @return The clear text username, if it could be extracted, null otherwise.
*/
protected String extractUser( String data )
{
String user = null ;
int start = data.indexOf( CAS_USER_BEGIN );
if ( start >= 0 )
{
start += CAS_USER_BEGIN.length();
int end = data.indexOf( CAS_USER_END );
if ( end > start )
user = data.substring( start, end );
else
LOG.warn( " Could not extract username from CAS validation response. Raw data is: ' " + data + " ' " );
} else
{
LOG.warn( " Could not extract username from CAS validation response. Raw data is: ' " + data + " ' " );
}
return user;
}
/**
* Helper method to extract the service ticket from a login call to CAS.
*
* @param data Response data.
* @return The service ticket, if it could be extracted, null otherwise.
*/
protected String extractServiceTicket( String data )
{
String serviceTicket = null ;
int start = data.indexOf( TICKET_BEGIN );
if ( start > 0 )
{
start += TICKET_BEGIN.length();
serviceTicket = data.substring( start );
}
return serviceTicket;
}
/**
* Helper method to extract the LT from a login form from CAS.
*
* @param data Response data.
* @return The LT, if it could be extracted, null otherwise.
*/
protected String extractLt( String data )
{
String token = null ;
int start = data.indexOf( LT_BEGIN );
if ( start < 0 )
{
LOG.error( " Could not obtain LT token from CAS: LT Token not found in response. " );
} else
{
start += LT_BEGIN.length();
int end = data.indexOf( """ , start );
token = data.substring( start, end );
}
return token;
}
/**
* This method requests the original login form from CAS.
* This form contains an LT, an initial token that must be
* presented to CAS upon sending it an authentication request
* with credentials.<br>
* If a service URL is provided (which is optional), this method
* will post the URL such that CAS authenticates against the
* specified service when a subsequent authentication request is
* sent.
*
* @param serviceUrl
* @return The LT token if it could be extracted from the CAS response.
*/
protected String getLt( String serviceUrl )
{
String lt = null ;
HttpMethod method = null ;
if ( serviceUrl == null )
method = new GetMethod( fCasUrl + LOGIN_URL_PART );
else
{
method = new PostMethod( fCasUrl + LOGIN_URL_PART );
( ( PostMethod ) method ).setParameter( " service " , serviceUrl );
}
try
{
int statusCode = fClient.executeMethod(method);
if (statusCode != HttpStatus.SC_OK)
{
LOG.error( " Could not obtain LT token from CAS: " + method.getStatusLine() );
method.releaseConnection();
} else
{
Object o = method.getResponseHeaders() ;
return extractLt( new String( method.getResponseBody() ) );
}
} catch ( Exception x )
{
LOG.error( " Could not obtain LT token from CAS: " + x.toString () );
}
method.releaseConnection();
return lt;
}
}
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
/**
* The CasClient allows users to programmatically login
* to CAS protected services based on the CAS 2 protocol.
* This client behaves like a browser-client in terms of
* cookie handling.<br>
*
* @author Mathias Richter
*/
public class CasClient
{
public static Logger LOG = Logger.getLogger( CasClient. class );
public static final String LOGIN_URL_PART = " login " ;
public static final String SERVICE_VALIDATE_URL_PART = " serviceValidate " ;
public static final String TICKET_BEGIN = " ticket= " ;
private static final String LT_BEGIN = " name= " lt " value= "" ;
public static final String CAS_USER_BEGIN = " <cas:user> " ;
public static final String CAS_USER_END = " </cas:user> " ;
private HttpClient fClient;
private String fCasUrl;
/**
* Construct a new CasClient.
*
* @param casUrl The base URL of the CAS service to be used.
*/
public CasClient( String casBaseUrl )
{
this ( new HttpClient(), casBaseUrl );
}
/**
* Construct a new CasClient which uses the specified HttpClient
* for its HTTP calls.
*
* @param client
* @param casBaseUrl
*/
public CasClient( HttpClient client, String casBaseUrl )
{
fClient = client;
fCasUrl = casBaseUrl;
}
/**
* Authenticate the specified username with the specified password.
* This will not yield any ticket, as no service is authenticated
* against. This wil just set the CAS cookie in this client upon
* successful authentication.
*
* @param username
* @param password
*/
public void authenticate( String username, String password )
{
authenticate( null , username, password );
}
/**
* Validate the specified service ticket against the specified service.
* If the ticket is valid, this will yield the clear text user name
* of the autenticated user.<br>
* Note that each service ticket issued by CAS can be used exactly once
* to validate.
*
* @param serviceUrl
* @param serviceTicket
*
* @return Clear text username of the authenticated user.
*/
public String validate( String serviceUrl, String serviceTicket )
{
String result = null ;
PostMethod method = new PostMethod( fCasUrl + SERVICE_VALIDATE_URL_PART );
method.setParameter( " service " , serviceUrl );
method.setParameter( " ticket " , serviceTicket );
try
{
int statusCode = fClient.executeMethod(method);
if (statusCode != HttpStatus.SC_OK)
{
LOG.error( " Could not validate: " + method.getStatusLine() );
method.releaseConnection();
} else
{
result = extractUser( new String( method.getResponseBody() ) );
}
} catch ( Exception x )
{
LOG.error( " Could not validate: " + x.toString () );
x.printStackTrace();
}
method.releaseConnection();
return result;
}
/**
* Authenticate the specified user with the specified password against the
* specified service.
*
* @param serviceUrl May be null. If a url is specified, the authentication will happen against this service, yielding a service ticket which can be validated.
* @param username
* @param password
* @return A valid service ticket, if and only if the specified service URL is not null.
*/
public String authenticate( String serviceUrl, String username, String password )
{
String lt = getLt( serviceUrl );
if ( lt == null )
{
LOG.error( " Cannot retrieve LT from CAS. Aborting authentication for ' " + username + " ' " );
return null ;
}
String result = null ;
PostMethod method = new PostMethod( fCasUrl + LOGIN_URL_PART );
if ( serviceUrl != null ) // optional
method.setParameter( " service " , serviceUrl );
method.setParameter( " _eventId " , " submit " );
method.setParameter( " username " , username );
method.setParameter( " password " , password );
method.setParameter( " lt " , lt );
method.setParameter( " gateway " , " true " );
try
{
fClient.executeMethod(method);
if ( serviceUrl == null )
{
if ( extractLt( new String( method.getResponseBody() ) ) != null ) // if CAS does not return a login page with an LT authentication was successful
{
LOG.error( " Authentication for ' " + username + " ' unsuccessful " );
if ( LOG.isDebugEnabled() )
LOG.debug( " Authentication for ' " + username + " ' unsuccessful. " );
} else
{
if ( LOG.isDebugEnabled() )
LOG.debug( " Authentication for ' " + username + " ' unsuccessful. " );
}
} else
{
Header h = method.getResponseHeader( " Location " );
if ( h != null )
result = extractServiceTicket( h.getValue() );
if ( result == null )
LOG.error( " Authentication for ' " + username + " ' unsuccessful. " );
}
} catch ( Exception x )
{
LOG.error( " Could not authenticate' " + username + " ': " + x.toString () );
}
method.releaseConnection();
return result;
}
/**
* Helper method to extract the user name from a "service validate" call to CAS.
*
* @param data Response data.
* @return The clear text username, if it could be extracted, null otherwise.
*/
protected String extractUser( String data )
{
String user = null ;
int start = data.indexOf( CAS_USER_BEGIN );
if ( start >= 0 )
{
start += CAS_USER_BEGIN.length();
int end = data.indexOf( CAS_USER_END );
if ( end > start )
user = data.substring( start, end );
else
LOG.warn( " Could not extract username from CAS validation response. Raw data is: ' " + data + " ' " );
} else
{
LOG.warn( " Could not extract username from CAS validation response. Raw data is: ' " + data + " ' " );
}
return user;
}
/**
* Helper method to extract the service ticket from a login call to CAS.
*
* @param data Response data.
* @return The service ticket, if it could be extracted, null otherwise.
*/
protected String extractServiceTicket( String data )
{
String serviceTicket = null ;
int start = data.indexOf( TICKET_BEGIN );
if ( start > 0 )
{
start += TICKET_BEGIN.length();
serviceTicket = data.substring( start );
}
return serviceTicket;
}
/**
* Helper method to extract the LT from a login form from CAS.
*
* @param data Response data.
* @return The LT, if it could be extracted, null otherwise.
*/
protected String extractLt( String data )
{
String token = null ;
int start = data.indexOf( LT_BEGIN );
if ( start < 0 )
{
LOG.error( " Could not obtain LT token from CAS: LT Token not found in response. " );
} else
{
start += LT_BEGIN.length();
int end = data.indexOf( """ , start );
token = data.substring( start, end );
}
return token;
}
/**
* This method requests the original login form from CAS.
* This form contains an LT, an initial token that must be
* presented to CAS upon sending it an authentication request
* with credentials.<br>
* If a service URL is provided (which is optional), this method
* will post the URL such that CAS authenticates against the
* specified service when a subsequent authentication request is
* sent.
*
* @param serviceUrl
* @return The LT token if it could be extracted from the CAS response.
*/
protected String getLt( String serviceUrl )
{
String lt = null ;
HttpMethod method = null ;
if ( serviceUrl == null )
method = new GetMethod( fCasUrl + LOGIN_URL_PART );
else
{
method = new PostMethod( fCasUrl + LOGIN_URL_PART );
( ( PostMethod ) method ).setParameter( " service " , serviceUrl );
}
try
{
int statusCode = fClient.executeMethod(method);
if (statusCode != HttpStatus.SC_OK)
{
LOG.error( " Could not obtain LT token from CAS: " + method.getStatusLine() );
method.releaseConnection();
} else
{
Object o = method.getResponseHeaders() ;
return extractLt( new String( method.getResponseBody() ) );
}
} catch ( Exception x )
{
LOG.error( " Could not obtain LT token from CAS: " + x.toString () );
}
method.releaseConnection();
return lt;
}
}