package net.unicon.idp.externalauth; import net.shibboleth.idp.authn.AuthnEventIds; import net.shibboleth.idp.authn.ExternalAuthentication; import net.shibboleth.idp.authn.ExternalAuthenticationException; import net.unicon.idp.authn.provider.extra.EntityIdParameterBuilder; import net.unicon.idp.authn.provider.extra.IParameterBuilder; 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.Cas30ServiceTicketValidator; import org.jasig.cas.client.validation.TicketValidationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; import org.springframework.web.context.WebApplicationContext; import javax.servlet.RequestDispatcher; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashSet; import java.util.Set; /** * 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 * @author jgasper@unicon.net */ @WebServlet(name = "ShibcasAuthServlet", urlPatterns = {"/Authn/ExtCas/*"}) public class ShibcasAuthServlet extends HttpServlet { private Logger logger = LoggerFactory.getLogger(ShibcasAuthServlet.class); private static final long serialVersionUID = 1L; private static final String artifactParameterName = "ticket"; private static final String serviceParameterName = "service"; private String casLoginUrl; private String serverName; private String casServerPrefix; private String ticketValidatorName; private String entityIdLocation; private Cas20ServiceTicketValidator ticketValidator; private Set<CasToShibTranslator> translators = new HashSet<CasToShibTranslator>(); private Set<IParameterBuilder> parameterBuilders = new HashSet<IParameterBuilder>(); @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { // TODO: We have the opportunity to give back more to Shib than just the PRINCIPAL_NAME_KEY. Identify additional information try { final String ticket = CommonUtils.safeGetParameter(request, artifactParameterName); final String gatewayAttempted = CommonUtils.safeGetParameter(request, "gatewayAttempted"); final String authenticationKey = ExternalAuthentication.startExternalAuthentication(request); final boolean force = Boolean.parseBoolean(request.getAttribute(ExternalAuthentication.FORCE_AUTHN_PARAM).toString()); final boolean passive = Boolean.parseBoolean(request.getAttribute(ExternalAuthentication.PASSIVE_AUTHN_PARAM).toString()); if ((ticket == null || ticket.isEmpty()) && (gatewayAttempted == null || gatewayAttempted.isEmpty())) { logger.debug("ticket and gatewayAttempted are not set; initiating CAS login redirect"); startLoginRequest(request, response, force, passive); return; } if (ticket == null || ticket.isEmpty()) { logger.debug("Gateway/Passive returned no ticket, returning NoPassive."); request.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, AuthnEventIds.NO_PASSIVE); ExternalAuthentication.finishExternalAuthentication(authenticationKey, request, response); return; } validateCasTicket(request, response, ticket, authenticationKey, force); } catch (final ExternalAuthenticationException e) { logger.warn("Error processing ShibCas authentication request", e); loadErrorPage(request, response); } catch (final Exception e) { logger.error("Something unexpected happened", e); request.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, AuthnEventIds.AUTHN_EXCEPTION); } } private void validateCasTicket(final HttpServletRequest request, final HttpServletResponse response, final String ticket, final String authenticationKey, final boolean force) throws ExternalAuthenticationException, IOException { try { ticketValidator.setRenew(force); String serviceUrl = constructServiceUrl(request, response); logger.debug("validating ticket: {} with service url: {}", ticket, serviceUrl); Assertion assertion = ticketValidator.validate(ticket, serviceUrl); if (assertion == null) { throw new TicketValidationException("Validation failed. Assertion could not be retrieved for ticket " + ticket); } for (CasToShibTranslator casToShibTranslator : translators) { casToShibTranslator.doTranslation(request, response, assertion); } ExternalAuthentication.finishExternalAuthentication(authenticationKey, request, response); } catch (final TicketValidationException e) { logger.error("Ticket validation failed, returning InvalidTicket", e); request.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, "InvalidTicket"); ExternalAuthentication.finishExternalAuthentication(authenticationKey, request, response); } } protected void startLoginRequest(final HttpServletRequest request, final HttpServletResponse response, Boolean force, Boolean passive) { // CAS Protocol - http://www.jasig.org/cas/protocol indicates not setting gateway if renew has been set. // we will set both and let CAS sort it out, but log a warning if (Boolean.TRUE.equals(passive) && Boolean.TRUE.equals(force)) { logger.warn("Both FORCE AUTHN and PASSIVE AUTHN were set to true, please verify that the requesting system has been properly configured."); } try { String serviceUrl = constructServiceUrl(request, response); if (passive) { serviceUrl += "&gatewayAttempted=true"; } String loginUrl = constructRedirectUrl(serviceUrl, force, passive) + getAdditionalParameters(request); logger.debug("loginUrl: {}", loginUrl); response.sendRedirect(loginUrl); } catch (final IOException e) { logger.error("Unable to redirect to CAS from ShibCas", e); } } /** * Uses the CAS CommonUtils to build the CAS Redirect URL. */ private String constructRedirectUrl(String serviceUrl, boolean renew, boolean gateway) { return CommonUtils.constructRedirectUrl(casLoginUrl, "service", serviceUrl, renew, gateway); } /** * Build addition querystring parameters * * @param request The original servlet request * @return an ampersand delimited list of querystring parameters */ private String getAdditionalParameters(final HttpServletRequest request) { StringBuilder builder = new StringBuilder(); for (IParameterBuilder paramBuilder : parameterBuilders) { builder.append(paramBuilder.getParameterString(request)); } return builder.toString(); } @Override public void init(ServletConfig config) throws ServletException { super.init(config); ApplicationContext ac = (ApplicationContext) config.getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); parseProperties(ac.getEnvironment()); switch (ticketValidatorName) { case "cas30": ticketValidator = new Cas30ServiceTicketValidator(casServerPrefix); break; case "cas20": ticketValidator = new Cas20ServiceTicketValidator(casServerPrefix); } if (ticketValidator == null) { throw new ServletException("Initialization failed. Invalid shibcas.ticketValidatorName property: '" + ticketValidatorName + "'"); } if ("append".equalsIgnoreCase(entityIdLocation)) { parameterBuilders.add(new EntityIdParameterBuilder()); } buildTranslators(ac.getEnvironment()); buildParameterBuilders(ac.getEnvironment()); } /** * Check the idp's idp.properties file for the configuration * * @param environment a Spring Application Context's Environment object (tied to the IdP's root context) */ private void parseProperties(Environment environment) { logger.debug("reading properties from the idp.properties file"); casServerPrefix = environment.getRequiredProperty("shibcas.casServerUrlPrefix"); logger.debug("shibcas.casServerUrlPrefix: {}", casServerPrefix); casLoginUrl = environment.getRequiredProperty("shibcas.casServerLoginUrl"); logger.debug("shibcas.casServerLoginUrl: {}", casLoginUrl); serverName = environment.getRequiredProperty("shibcas.serverName"); logger.debug("shibcas.serverName: {}", serverName); ticketValidatorName = environment.getProperty("shibcas.ticketValidatorName", "cas30"); logger.debug("shibcas.ticketValidatorName: {}", ticketValidatorName); entityIdLocation = environment.getProperty("shibcas.entityIdLocation", "append"); logger.debug("shibcas.entityIdLocation: {}", entityIdLocation); } private void buildParameterBuilders(final Environment environment) { String builders = StringUtils.defaultString(environment.getProperty("shibcas.parameterBuilders", "")); for (String parameterBuilder : StringUtils.split(builders, ";")) { try { logger.debug("Loading parameter builder class {}", parameterBuilder); Class clazz = Class.forName(parameterBuilder); this.parameterBuilders.add(IParameterBuilder.class.cast(clazz.newInstance())); logger.debug("Added parameter builder {}", parameterBuilder); } catch (Throwable e) { logger.error("Error building parameter builder with name: " + parameterBuilder, e); } } } /** * 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(Environment environment) { translators.add(new AuthenticatedNameTranslator()); String casToShibTranslators = StringUtils.defaultString(environment.getProperty("shibcas.casToShibTranslators", "")); for (String classname : StringUtils.split(casToShibTranslators, ';')) { try { logger.debug("Loading translator class {}", classname); Class<?> c = Class.forName(classname); translators.add((CasToShibTranslator) c.newInstance()); logger.debug("Added translator class {}", classname); } 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) { String serviceUrl = CommonUtils.constructServiceUrl(request, response, null, serverName, serviceParameterName, artifactParameterName, true); if ("embed".equalsIgnoreCase(entityIdLocation)) { serviceUrl += (new EntityIdParameterBuilder().getParameterString(request, false)); } return serviceUrl; } private void loadErrorPage(final HttpServletRequest request, final HttpServletResponse response) { RequestDispatcher requestDispatcher = request.getRequestDispatcher("/no-conversation-state.jsp"); try { requestDispatcher.forward(request, response); } catch (Exception e) { logger.error("Error rendering the empty conversation state (shib-cas-authn3) error view."); response.resetBuffer(); response.setStatus(404); } } }