jasigsakai12 columbia-customizes-cas
DESCRIPTION
TRANSCRIPT
June 10-15, 2012
Growing Community; Growing Possibilities
Dan Ellentuck, Columbia University
Bill Thompson, Unicon Inc.
Reasons to Choose CAS:Google Apps SSOSAML SupportVendor SupportCommunity SupportTie-in with other open source tools and products, e.g.,
Sakai
Complicating Factors:Pre-existing local web auth systemActive, diverse client base
Question:How can legacy system be migrated to CAS?
CAS support for Google Apps SSO
Migrating a pre-existing web auth system to CAS
CAS customizations and enhancements:• Adding support for a new protocol
• Plugging in a custom service registry
• Enabling per-service UI tweaks
• Changing some basic login behavior
Google Apps SSO is based on SAML 2. See: https://developers.google.com/google-apps/sso/saml_reference_implementation
Step-by-step instructions on configuring CAS for Google Apps sso: https://wiki.jasig.org/pages/viewpage.action?pageId=6063484
Works OOTB.
Sibling of CAS, called “WIND”. Cookie-based SSO. No generic login. Per-service UI customization and opt-in SSO. Similar APIs with different request param names:
WIND:
/login?destination=https://MY-APPLICATION-PATH
/logout
/validate?ticketid=SERVICE-TICKET
CAS:
/login?service=https://MY-APPLICATION-PATH
/logout
/serviceValidate?service=https://APPLICATION-PATH&ticket=SERVICE-TICKET
2 private validation response formats (text and xml):
<wind:serviceResponse
xmlns:wind='http://www.columbia.edu/acis/rad/authmethods/wind'>
<wind:authenticationSuccess>
<wind:user>de3</wind:user>
<wind:passwordtyped>true</wind:passwordtyped>
<wind:logintime>1338696023</wind:logintime>
<wind:passwordtime>1331231507</wind:passwordtime>
<wind:passwordchangeURI>https://idmapp.cc.columbia.edu/acctmanage/changepasswd
</wind:passwordchangeURI>
</wind:authenticationSuccess>
</wind:serviceResponse>
yes
de3
Service registry with maintenance UI Service attributes for UI customization, multiple destinations,
attribute release, application contacts, etc.
SINGLE_SIGN_ON (T/F)PROXY_GRANTING (T/F)RETURN_XML (T/F)ID_FORMATDESCRIPTIONHELP_URI (for customizing UI)IMAGE_PATH(for customizing UI )HELP_LABEL(for customizing UI)
SERVICE DESTINATION
DESTINATION
SERVICE_CONTACT
EMAIL_ADDRESS
CONTACT_TYPE
AFFILIATION (like ATTRIBUTE)
AFFILIATION
SERVICE_LABEL
SERVICE_LABEL
SERVICE_LABEL
SERVICE_LABEL
Collaboration between Columbia and Unicon.
Tasks:◦ Plug legacy service registry into CAS.◦ Add legacy authentication protocol to CAS.◦ Port login UI customizations to CAS.◦ Change some login behavior (eliminate generic login.)
New service registrations must use CAS protocol.
Existing clients can use either legacy or CAS protocols during transition.
• Java
• View technologies (JSP, CSS, etc.)
• Maven (dependencies; overlays)
• Spring configuration (CAS set up)
• Spring Web Flow (SWF)
• App server/web server (tomcat/apache)
Service Registry is obvious extension point.
Advantages to plugging in local service registry:◦ Retain extended service attributes and functions
◦ Remove migration headache
◦ Can continue to use legacy maintenance UI
public interface WindRegisteredService extends RegisteredService {
/**
* Returns a display label for the help link. Can be null.
* Ignored if getHelpUri() is null.
* @return String
*/
String getHelpLabel();
/**
* Returns a help URI. Can be null.
* @return String
*/
String getHelpUri();
...etc.
}
Step 1: Write a CAS RegisteredService adaptor, part 1. Write an interface that extends CAS RegisteredService with any extra attributes in the custom service registry.
public class WindRegisteredServiceImpl implements WindRegisteredService,
Comparable<RegisteredService> {
public boolean matches(Service targetService) {
if (!isEnabled() || targetService == null ||
targetService.getId() == null || targetService.getId().isEmpty())
return false;
for (String registeredDestination :
List<String>) getWindService().getAllowed_destinations()) {
String target = targetService.getId().substring(0,
registeredDestination.length());
if (registeredDestination.equalsIgnoreCase(target))
return true;
}
return false;
}
...
}
Step 2: Write a CAS RegisteredService adaptor, part 2. Write a RegisteredService implementation that adapts an instance of the custom service to the extended RegisteredService interface.
public class ReadOnlyWindServicesManagerImpl implements ReloadableServicesManager
{
...
public RegisteredService findServiceBy(Service targetService) {
edu.columbia.acis.rad.wind.model.Service windService =
findWindService(targetService);
return ( windService != null )
? getRegisteredServicesByName().get(windService.getLabel())
: null;
}
public RegisteredService findServiceBy(long id) {
return getRegisteredServicesById().get(id);
}
...
}
Step 3: Implement a CAS ServicesManager (maps incoming Service URL of a request with the matching CAS RegisteredService.)
applicationContext.xml
<!–
Default servicesManager bean definition replaced by custom servicesManager
<bean
id="servicesManager"
class="org.jasig.cas.services.DefaultServicesManagerImpl">
<constructor-arg index="0" ref="serviceRegistryDao"/>
</bean>
-->
<bean
id="servicesManager"
class="edu.columbia.acis.rad.wind.cas.ReadOnlyWindServicesManagerImpl">
<constructor-arg index=“0” ref =“wind-ServicesCollection"/>
</bean>
...etc.
Step 4: Write Spring bean definitions for the new ServicesManager.
Result…
Additional service attributes and functions are available to CAS
Custom maintenance UI can be used
Service registry uses custom logic to match Service URL of incoming request with appropriate registered service.
Easy migration
CAS is multi-protocol
Wind and CAS protocols are similar but not identical
Different servlet API and validation response formats
Advantages to adding legacy protocol to CAS:◦ Single authentication service
◦ Single SSO domain
◦ Easy migration from legacy system
public class WindService extends AbstractWebApplicationService {
private static final String DESTINATION_PARAM = "destination";
private static final String SERVICE_PARAM = "service";
private static final String TICKET_PARAM = "ticketid";
...
// Create a Service instance from the request:
public static WindService from(HttpServletRequest request, HttpClient httpClient)
{
String origUrl = request.getParameter(DESTINATION_PARAM);
...
new WindService(origUrl, origUrl, /*artifactId not used*/ null, httpClient);
}
Step 1: Implement the CAS Service interface for the new protocol by subclassing abstractWebApplicationService:
Step 2: Write an ArgumentExtractor class to retrieve values of protocol-specific request parameters and return instances of the Service class created in Step 1:
public class WindArgumentExtractor extends AbstractSingleSignOutEnabledArgumentExtractor
{
private static final String TICKET_PARAM = "ticketid";
...
protected WebApplicationService extractServiceInternal
( HttpServletRequest request)
//Coming in from validation request
if ("/validate".equals(request.getServletPath())) {
String ticketId = request.getParameter(TICKET_PARAM);
ServiceTicket st = (ServiceTicket)
this.ticketRegistry.getTicket(ticketId, ServiceTicket.class);
WindService ws = st != null ? (WindService) st.getService() : null;
...
return WindService.from(ticketId, ws., getHttpClientIfSingleSignOutEnabled());
Step 3: In web.xml, map the servlet path for the protocol’s version of the service ticket validation request to the cas servlet:
<servlet>
<servlet-name>cas</servlet-name>
<servlet-class>
org.jasig.cas.web.init.SafeDispatcherServlet
</servlet-class>
<init-param>
<param-name>publishContext</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
...
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/validate</url-pattern>
</servlet-mapping>
...
Step 4: Write a view class to format the service ticket validation response:
class WindResponseView extends AbstractCasView {
....
private buildSuccessXmlResponse(Assertion assertion) {
def auth = assertion.chainedAuthentications[0]
def principalId = auth.principal.id
def xmlOutput = new StreamingMarkupBuilder()
xmlOutput.bind {
mkp.declareNamespace('wind': WIND_XML_NAMESPACE)
wind.serviceResponse {
wind.authenticationSuccess {
wind.user(principalId)
wind.passwordtyped(assertion.fromNewLogin)
wind.logintime(auth.authenticatedDate.time)
...etc.
}
}
}.toString()
}
Step 5: Define and wire up beans for the various protocol operations:
argumentExtractorsConfiguration.xml
defines ArgumentExtractor classes for the various supported protocols:
<bean id="windArgumentExtractor"
class="edu.columbia.cas.wind.WindArgumentExtractor"
p:httpClient-ref="httpClient"
p:disableSingleSignOut="true">
<constructor-arg index="0" ref="ticketRegistry"/>
</bean>
uniqueIdGenerators.xml
protocol is mapped to uniqueID generator for service tickets via Service class:
<util:map id=“uniqueIdGeneratorsMap”>
<entry key=“edu.columbia.cas.wind.WindService”
value-ref=“serviceTicketUniqueIdGenerator” />
...etc.
</util:map>
Step 5: Define and wire up beans for the various protocol operations (cont’d):
cas-servlet.xml
bean definitions made available to the web flow:
<prop
key=“/validate”>
windValidateController
</prop
...
<bean id=“windValidateController”
class=“org.jasig.cas.web.ServiceValidateController”
p:proxyHandler-ref=“proxy20Handler”
p:successView=“windServiceSuccessView”
p:failureView=“windServiceFailureView”
p:validationSpecificationClass=
“org.jasig.cas.validation.Cas20WithoutProxyingValidationSpecification”
p:centralAuthenticationService-ref=“centralAuthenticationService”
p:argumentExtractor-ref=“windArgumentExtractor”/>
...etc.
2012 Jasig Sakai Conference 23
Result…
CAS will detect a request in the new protocol;
Extract appropriate request parameters;
Respond in the appropriate format.
Legacy clients continue to use usual auth protocol until ready to migrate.
Single server/SSO realm.
Adding local images and content to the CAS login UI is a common implementation step.
CAS lets each RegisteredService have its own style sheet (high effort.)
Legacy auth service allows per-service tweaks to the login UI (low effort):
• Custom logo
• Help link and help label
• Choice of displaying institutional links
• Popular with clients
Prerequisite:
◦ Must have service-specific attributes that control the customization.
◦ Extend service registry with custom UI elements; or
◦ Plug in custom service registry (see above.)
Public class ServiceUiElementsResolverAction extends AbstractAction {
...
protected Event doExecute(RequestContext requestContext) throws Exception {
// get the Service from requestContext.
Service service = (Service) requestContext.getFlowScope().get("service",
Service.class);
...
// get the RegisteredService for this request from the ServicesManager.
WindRegisteredService registeredService = (WindRegisteredService)
this.servicesManager.findServiceBy(service);
...
// make RegisteredService available to the view.
requestContext.getRequestScope().put("registeredService",
registeredService);
...
}
...
}
Step 1: Write a Spring Web Flow Action class to map the incoming Service to a RegisteredService and make the RegisteredService available in the web flow context.
cas-servlet.xml
...
<bean id="uiElementsResolverAction“
class="edu.columbia.cas.wind.ServiceUiElementsResolverAction">
<constructor-arg index="0" ref=“servicesManager"/>
</bean>
Step 2: Define a bean for the Action class in cas-servlet.xml, to make the class available to the login web flow:
Login-webflow.xml
...
<view-state id="viewLoginForm" view="casLoginView" model="credentials">
<binder>
<binding property="username" />
<binding property="password" />
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credentials'" />
<!– Make RegisteredService available in web flow context -->
<evaluate expression="uiElementsResolverAction"/>
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmit">
<evaluate expression="authenticationViaFormAction.doBind
(flowRequestContext, flowScope.credentials)" />
</transition>
</view-state>
Step 3: Make the RegisteredService available to the web flow by doing our Action in the login web flow just before the login UI is rendered:
casLoginView.jsp
...
<!-- Derive the path to the logo image from the registered service. -->
<c:set var="imagePath" value =
"${!empty registeredService.imagePath
? registeredService.imagePath : defaultImagePath}"/>
...
<!-- display the custom logo -->
<img src="<c:url value="${imagePath}" />" alt="${registeredService.name}"
/>
...
Step 4: In the login view, refer to RegisteredServiceattributes when customizing the UI markup:
Result…
◦ Vanilla login page
◦ Login page with default logo, institutional links
◦ Login page with custom logo
◦ Login page with another custom logo and help link
CAS allows a login without a service, a genericlogin, which creates a ticket granting ticket but no service ticket.
Generic login permitted
Legacy auth service assumes client is always trying to log into something. Treats a generic login as an error. We want to preserve this behavior.
public class CheckForRegisteredServiceAction extends AbstractAction {
ServicesManager servicesManager;
protected Event doExecute(RequestContext requestContext)
throws Exception
{
Service service = (Service)
requestContext.getFlowScope().get("service", Service.class);
RegisteredService registeredService = null;
if(service != null) {
registeredService = this.servicesManager.findServiceBy(service);
}
return ( registeredService==null ) ? error() : success();
}
}
Step 1: Write a Spring Web Flow Action that checks if the login request has a known service destination and returns success/error.
cas-servlet.xml
...
<bean id="checkForRegisteredServiceAction“
class="edu.columbia.cas.wind.CheckForRegisteredServiceAction">
<constructor-arg index="0" ref="servicesManager"/>
</bean>
...
Step 2: Make the class available to the login web flow by defining a bean in cas-servlet.xml:
login-webflow.xml...
<!-- validate the request: non-null service with corresponding RegisteredService -->
<decision-state id="hasServiceCheck">
<if test="flowScope.service != null" then="hasRegisteredServiceCheck“
else="viewServiceErrorView" />
</decision-state>
<!-- Is there a corresponding RegisteredService? -->
<action-state id="hasRegisteredServiceCheck">
<evaluate expression="checkForRegisteredServiceAction"/>
<transition on="success" to="ticketGrantingTicketExistsCheck" />
<transition on="error" to="viewServiceErrorView" />
</action-state>
Step 3: In the login web flow add an action-state to check that the request has a service parameter, and it corresponds
to a RegisteredService.
Result…
◦ CAS will now assume client is always trying to log into something and treat a request without a known service destination as an error.
◦ Users will not see login UI less they arrive with a registered service.
◦ Generic login not permitted
Tasks accomplished:
◦ Support Google Apps SSO
◦ Plug legacy service registry into CAS
◦ Add legacy authentication protocol to CAS
◦ Port login UI customizations to CAS
◦ Eliminate generic login
Dan Ellentuck, Columbia [email protected]
Bill Thompson, Unicon [email protected]