package net.unicon.idp.authn.provider; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Constructor; import java.util.HashSet; import java.util.Properties; import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import net.unicon.idp.authn.provider.extra.EntityIdParameterBuilder; import net.unicon.idp.authn.provider.extra.IParameterBuilder; import net.unicon.idp.externalauth.CasCallbackServlet; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.internet2.middleware.shibboleth.idp.authn.LoginContext; import edu.internet2.middleware.shibboleth.idp.authn.provider.AbstractLoginHandler; import edu.internet2.middleware.shibboleth.idp.authn.provider.ExternalAuthnSystemLoginHandler; import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper; /** * CasLoginHandler replaces the {@link CasInvokerServlet} AND {@link CasAuthenticatorResource} (facade) from the v1.x implementations. * Allows simplification of the SHIB-CAS authenticator by removing the need to configure and deploy a separate war. * * This LoginHandler handles taking the login request from Shib and translating and sending the request on to the CAS instance. * @author chasegawa@unicon.net */ public class CasLoginHandler extends AbstractLoginHandler { private static final String MISSING_CONFIG_MSG = "Unable to create CasLoginHandler - missing {} property. Please check {}"; private static final String LOGIN = "/login"; private static final Logger LOGGER = LoggerFactory.getLogger(CasLoginHandler.class); private String callbackUrl; private String casLoginUrl; private String casProtocol = "https"; private String casPrefix = "/cas"; private String casServer; private String idpProtocol = "https"; private String idpServer; private String idpPrefix = "/idp"; private String idpCallback = "/Authn/Cas"; private Set<IParameterBuilder> parameterBuilders = new HashSet<IParameterBuilder>(); { // By default, we start with the entity id param builder included parameterBuilders.add(new EntityIdParameterBuilder()); } /** * Create a new instance of the login handler. Read the configuration properties from the properties file indicated as * a construction argument. * @param propertiesFile File and path name to the file containing the required properties: * <li>cas.server * <li>idp.server * @throws FileNotFoundException */ public CasLoginHandler(final String propertiesFile, final String paramBuilderNames) throws FileNotFoundException { this(new FileReader(new File(propertiesFile)), propertiesFile, paramBuilderNames); } /** * Construct a new instance using the supplied parameters. * @param propertiesFileReader The reader to the properties file * @param propertiesFile The name of the properties file (used for logging and error messages) * @param paramBuilderNames The list of parameter builder names */ public CasLoginHandler(final Reader propertiesFileReader, final String propertiesFile, final String paramBuilderNames) { Properties props = new Properties(); try { if (null == propertiesFileReader) { throw new FileNotFoundException("Error reading properties file: " + propertiesFile); } try { props.load(propertiesFileReader); propertiesFileReader.close(); } catch (final IOException e) { LOGGER.debug("Error reading properties file: {}", propertiesFile); throw e; } 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; casLoginUrl = casProtocol + "://" + casServer + casPrefix + LOGIN; temp = getProperty(props, "idp.server.protocol"); idpProtocol = StringUtils.isEmpty(temp) ? idpProtocol : temp; temp = getProperty(props, "idp.server"); idpServer = StringUtils.isEmpty(temp) ? idpServer : temp; temp = getProperty(props, "idp.application.prefix"); idpPrefix = StringUtils.isEmpty(temp) ? idpPrefix : temp; temp = getProperty(props, "idp.server.callback"); idpCallback = StringUtils.isEmpty(temp) ? idpCallback : temp; callbackUrl = idpProtocol + "://" + idpServer + idpPrefix + idpCallback; } catch (final Exception e) { LOGGER.error("Unable to load parameters", e); throw new RuntimeException(e); } if (StringUtils.isEmpty(casServer)) { LOGGER.error(MISSING_CONFIG_MSG, "cas.server", propertiesFile); throw new IllegalArgumentException( "CasLoginHandler missing properties needed to build the cas login URL in handler configuration."); } if (null == idpServer || "".equals(idpServer.trim())) { LOGGER.error(MISSING_CONFIG_MSG, "idp.server", propertiesFile); throw new IllegalArgumentException( "CasLoginHandler missing properties needed to build the callback URL in handler configuration."); } setSupportsForceAuthentication(true); setSupportsPassive(true); createParamBuilders(paramBuilderNames); } /** * @param paramBuilderNames The comma separated list of class names to create. */ private void createParamBuilders(final String paramBuilderNames) { for (String className : StringUtils.split(paramBuilderNames, ',')) { try { Class<?> c = Class.forName(className); Constructor<?> cons = c.getConstructor(); parameterBuilders.add((IParameterBuilder) cons.newInstance()); } catch (Exception e) { LOGGER.warn("Unable to create IParameterBuilder with classname {}", className, e); } } } /** * @param request The original servlet request * @return */ private String getAdditionalParameters(final HttpServletRequest request) { StringBuilder builder = new StringBuilder(); for (IParameterBuilder paramBuilder : parameterBuilders) { builder.append(paramBuilder.getParameterString(request)); } return builder.toString(); } /** * @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; } /** * Translate the SHIB request so that cas renew and/or gateway are set properly before handing off to CAS. * @see edu.internet2.middleware.shibboleth.idp.authn.LoginHandler#login(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public void login(final HttpServletRequest request, final HttpServletResponse response) { ServletContext application = request.getSession().getServletContext(); LoginContext loginContext = (LoginContext) HttpServletHelper.getLoginContext( HttpServletHelper.getStorageService(application), application, request); Boolean force = loginContext.isForceAuthRequired(); // CAS Protocol - http://www.jasig.org/cas/protocol recommends that when this param is set, to set "true" String authnType = force ? "renew=true" : ""; Boolean passive = loginContext.isPassiveAuthRequired(); // 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 (passive) { if (Boolean.TRUE.equals(force)) { authnType += "&"; LOGGER.warn("Both FORCE AUTHN and PASSIVE AUTHN were set to true, please verify that the requesting system has been properly configured."); } authnType += "gateway=true"; } try { HttpSession session = request.getSession(); // Coupled this attribute to the CasCallbackServlet as that is the type that needs this bit of information session.setAttribute(CasCallbackServlet.AUTHN_TYPE, authnType); // Create the raw login string - Service/Callback URL should always be last StringBuilder loginString = new StringBuilder(casLoginUrl + "?"); loginString.append(authnType); String additionalParams = getAdditionalParameters(request); if (StringUtils.endsWith(loginString.toString(), "?")) { additionalParams = StringUtils.removeStart(additionalParams, "&"); } loginString.append(additionalParams); loginString.append(StringUtils.endsWith(loginString.toString(), "?") ? "service=" : "&service="); loginString.append(callbackUrl); response.sendRedirect(response.encodeRedirectURL(loginString.toString())); } catch (final IOException e) { LOGGER.error("Unable to redirect to CAS from LoginHandler", e); } } }