package net.unicon.idp.externalauth; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.HashSet; import java.util.Properties; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.validation.Assertion; import org.jasig.cas.client.validation.Cas20ServiceTicketValidator; import org.jasig.cas.client.validation.TicketValidationException; import org.opensaml.saml2.core.StatusCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.internet2.middleware.shibboleth.idp.authn.AuthenticationEngine; import edu.internet2.middleware.shibboleth.idp.authn.LoginHandler; /** * A Servlet that validates the CAS ticket and then pushes the authenticated principal name into the correct location before * handing back control to Shib * * @author chasegawa@unicon.net */ public class CasCallbackServlet extends HttpServlet { public static final String AUTHN_TYPE = "authnType"; private static final String DEFAULT_CAS_SHIB_PROPS = "/opt/shibboleth-idp/conf/cas-shib.properties"; private static final long serialVersionUID = 1L; private String artifactParameterName = "ticket"; private String casPrefix = "/cas"; private String casProtocol = "https"; private String casServer; private String casToShibTranslatorNames; private String idpProtocol = "https"; private String idpServer; private Logger logger = LoggerFactory.getLogger(CasCallbackServlet.class); private String serverName; private Cas20ServiceTicketValidator ticketValidator; private Set<CasToShibTranslator> translators = new HashSet<CasToShibTranslator>(); /** * Attempt to build the set of translators from the fully qualified class names set in the properties. If nothing has been set * then default to the AuthenticatedNameTranslator only. */ private void buildTranslators() { translators.add(new AuthenticatedNameTranslator()); for (String classname : StringUtils.split(casToShibTranslatorNames, ';')) { try { Class<?> c = Class.forName(classname); Constructor<?> cons = c.getConstructor(); CasToShibTranslator casToShibTranslator = (CasToShibTranslator) cons.newInstance(); translators.add(casToShibTranslator); } catch (Exception e) { logger.error("Error building cas to shib translator with name: " + classname, e); } } } /** * Use the CAS CommonUtils to build the CAS Service URL. */ private String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response) { return CommonUtils.constructServiceUrl(request, response, null, serverName, artifactParameterName, true); } /** * @TODO: We have the opportunity to give back more to Shib than just the PRINCIPAL_NAME_KEY. Identify additional information * we can return as well as the best way to know when to do this. * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { String ticket = CommonUtils.safeGetParameter(request, artifactParameterName); Object authnType = request.getSession().getAttribute(AUTHN_TYPE); Assertion assertion = null; try { ticketValidator.setRenew(null != authnType && authnType.toString().contains("&renew=true")); assertion = ticketValidator.validate(ticket, constructServiceUrl(request, response)); } catch (final TicketValidationException e) { logger.error("Unable to validate login attempt.", e); boolean wasPassiveAttempt = null != authnType && authnType.toString().contains("&gateway=true"); // If it was a passive attempt, send back the indicator that the responding provider cannot authenticate // the principal passively, as has been requested. Otherwise, send the generic authn failed code. request.setAttribute(LoginHandler.AUTHENTICATION_ERROR_KEY, wasPassiveAttempt ? StatusCode.NO_PASSIVE_URI : StatusCode.AUTHN_FAILED_URI); AuthenticationEngine.returnToAuthenticationEngine(request, response); return; } for (CasToShibTranslator casToShibTranslator : translators) { casToShibTranslator.doTranslation(request, response, assertion); } AuthenticationEngine.returnToAuthenticationEngine(request, response); } /** * @return the init param value or empty string if the key/value isn't found */ private String getInitParamByName(final String key) { String result = getInitParameter(key); return StringUtils.isEmpty(result) ? "" : result; } /** * @return the property value or empty string if the key/value isn't found */ private String getProperty(final Properties props, final String key) { String result = props.getProperty(key); return StringUtils.isEmpty(result) ? "" : result; } /** * @see javax.servlet.GenericServlet#init() */ @Override public void init() throws ServletException { super.init(); parseProperties(); buildTranslators(); } /** * Check for the externalized properties first. If this hasn't been set, go with the default filename/path * If we are unable to load the parameters, we will attempt to load from the init-params. Missing parameters will * cause an error - we will not attempt to mix initialization between props and init-params. * @throws ServletException */ private void parseProperties() throws ServletException { String casUrlPrefix = null; String artifactParamaterName = null; String fileName = getInitParamByName("propertiesFile"); if (null == fileName || "".equals(fileName.trim())) { logger.debug("propertiesFile init-param not set, defaulting to " + DEFAULT_CAS_SHIB_PROPS); fileName = DEFAULT_CAS_SHIB_PROPS; } Properties props = new Properties(); try { try { FileReader reader = new FileReader(new File(fileName)); props.load(reader); reader.close(); } catch (final FileNotFoundException e) { logger.debug("Unable to locate file: " + fileName); throw e; } catch (final IOException e) { logger.debug("Error reading file: " + fileName); throw e; } logger.debug("Attempting to load parameters from properties file"); String temp = getProperty(props, "cas.server.protocol"); casProtocol = StringUtils.isEmpty(temp) ? casProtocol : temp; temp = getProperty(props, "cas.application.prefix"); casPrefix = StringUtils.isEmpty(temp) ? casPrefix : temp; temp = getProperty(props, "cas.server"); casServer = StringUtils.isEmpty(temp) ? casServer : temp; temp = getProperty(props, "idp.server.protocol"); idpProtocol = StringUtils.isEmpty(temp) ? idpProtocol : temp; temp = getProperty(props, "idp.server"); idpServer = StringUtils.isEmpty(temp) ? idpServer : temp; artifactParamaterName = getProperty(props, "artifact.parameter.name"); casToShibTranslatorNames = getProperty(props, "casToShibTranslators"); } catch (final Exception e) { logger.debug("Error reading properties, attempting to load parameters from servlet init-params"); String temp = getInitParamByName("cas.server.protocol"); casProtocol = StringUtils.isEmpty(temp) ? casProtocol : temp; temp = getInitParamByName("cas.application.prefix"); casPrefix = StringUtils.isEmpty(temp) ? casPrefix : temp; temp = getInitParamByName("cas.server"); casServer = StringUtils.isEmpty(temp) ? casServer : temp; temp = getInitParamByName("idp.server.protocol"); idpProtocol = StringUtils.isEmpty(temp) ? idpProtocol : temp; temp = getInitParamByName("idp.server"); idpServer = StringUtils.isEmpty(temp) ? idpServer : temp; artifactParamaterName = getInitParamByName("artifact.parameter.name"); casToShibTranslatorNames = getInitParamByName("casToShibTranslators"); } if (StringUtils.isEmpty(casServer)) { logger.error("Unable to start CasCallbackServlet. Verify that the IDP's web.xml file OR the external property is configured properly."); throw new ServletException( "Missing casServer parameter to build the cas server URL - this is a required value"); } casUrlPrefix = casProtocol + "://" + casServer + casPrefix; ticketValidator = new Cas20ServiceTicketValidator(casUrlPrefix); if (StringUtils.isEmpty(idpServer)) { logger.error("Unable to start CasCallbackServlet. Verify that the IDP's web.xml file OR the external property is configured properly."); throw new ServletException( "Missing idpServer parameter to build the idp server URL - this is a required value"); } serverName = idpProtocol + "://" + idpServer; artifactParameterName = (StringUtils.isEmpty(artifactParamaterName) || "null".equals(artifactParamaterName)) ? "ticket" : artifactParamaterName; } }