/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.picketlink.identity.federation.bindings.tomcat.sp; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.security.GeneralSecurityException; import java.security.Principal; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.crypto.dsig.CanonicalizationMethod; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.Session; import org.apache.catalina.authenticator.AuthenticatorBase; import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.deploy.LoginConfig; import org.picketlink.identity.federation.PicketLinkLogger; import org.picketlink.identity.federation.PicketLinkLoggerFactory; import org.picketlink.identity.federation.api.saml.v2.metadata.MetaDataExtractor; import org.picketlink.identity.federation.core.ErrorCodes; import org.picketlink.identity.federation.core.audit.PicketLinkAuditHelper; import org.picketlink.identity.federation.core.config.PicketLinkType; import org.picketlink.identity.federation.core.config.SPType; import org.picketlink.identity.federation.core.exceptions.ConfigurationException; import org.picketlink.identity.federation.core.exceptions.ParsingException; import org.picketlink.identity.federation.core.exceptions.ProcessingException; import org.picketlink.identity.federation.core.handler.config.Handlers; import org.picketlink.identity.federation.core.interfaces.TrustKeyManager; import org.picketlink.identity.federation.core.parsers.saml.SAMLParser; import org.picketlink.identity.federation.core.saml.v2.constants.JBossSAMLURIConstants; import org.picketlink.identity.federation.core.saml.v2.factories.SAML2HandlerChainFactory; import org.picketlink.identity.federation.core.saml.v2.impl.DefaultSAML2HandlerChainConfig; import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2Handler; import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2HandlerChain; import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2HandlerChainConfig; import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil; import org.picketlink.identity.federation.core.saml.v2.util.HandlerUtil; import org.picketlink.identity.federation.core.util.CoreConfigUtil; import org.picketlink.identity.federation.core.util.StringUtil; import org.picketlink.identity.federation.core.util.SystemPropertiesUtil; import org.picketlink.identity.federation.core.util.XMLSignatureUtil; import org.picketlink.identity.federation.saml.v2.metadata.EndpointType; import org.picketlink.identity.federation.saml.v2.metadata.EntitiesDescriptorType; import org.picketlink.identity.federation.saml.v2.metadata.EntityDescriptorType; import org.picketlink.identity.federation.saml.v2.metadata.IDPSSODescriptorType; import org.picketlink.identity.federation.saml.v2.metadata.KeyDescriptorType; import org.picketlink.identity.federation.web.config.AbstractSAMLConfigurationProvider; import org.picketlink.identity.federation.web.constants.GeneralConstants; import org.picketlink.identity.federation.web.util.ConfigurationUtil; import org.picketlink.identity.federation.web.util.SAMLConfigurationProvider; import org.w3c.dom.Document; /** * Base Class for Service Provider Form Authenticators * * @author Anil.Saldhana@redhat.com * @since Jun 9, 2009 */ public abstract class BaseFormAuthenticator extends FormAuthenticator { protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger(); protected boolean enableAudit = false; protected PicketLinkAuditHelper auditHelper = null; protected TrustKeyManager keyManager; protected SPType spConfiguration = null; protected PicketLinkType picketLinkConfiguration = null; protected String serviceURL = null; protected String identityURL = null; protected String issuerID = null; protected String configFile = GeneralConstants.CONFIG_FILE_LOCATION; /** * If the service provider is configured with an IDP metadata file, then this certificate can be picked up from the metadata */ protected transient X509Certificate idpCertificate = null; protected transient SAML2HandlerChain chain = null; protected transient String samlHandlerChainClass = null; protected Map<String, Object> chainConfigOptions = new HashMap<String, Object>(); // Whether the authenticator has to to save and restore request protected boolean saveRestoreRequest = true; /** * A Lock for Handler operations in the chain */ protected Lock chainLock = new ReentrantLock(); protected String canonicalizationMethod = CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS; /** * The user can inject a fully qualified name of a {@link SAMLConfigurationProvider} */ protected SAMLConfigurationProvider configProvider = null; /** * Servlet3 related changes forced Tomcat to change the authenticate method signature in the FormAuthenticator. For now, we * use reflection for forward compatibility. This has to be changed in future. */ private Method theSuperRegisterMethod = null; /** * If it is determined that we are running in a Tomcat6/JBAS5 environment, there is no need to seek the super.register * method that conforms to the servlet3 spec changes */ private boolean seekSuperRegisterMethod = true; public BaseFormAuthenticator() { super(); } protected String idpAddress = null; /** * If the request.getRemoteAddr is not exactly the IDP address that you have keyed in your deployment descriptor for * keystore alias, you can set it here explicitly */ public void setIdpAddress(String idpAddress) { this.idpAddress = idpAddress; } /** * Get the name of the configuration file * @return */ public String getConfigFile() { return configFile; } /** * Set the name of the configuration file * @param configFile */ public void setConfigFile(String configFile) { this.configFile = configFile; } /** * Set the SAML Handler Chain Class fqn * @param samlHandlerChainClass */ public void setSamlHandlerChainClass(String samlHandlerChainClass) { this.samlHandlerChainClass = samlHandlerChainClass; } /** * Set the service URL * @param serviceURL */ public void setServiceURL(String serviceURL) { this.serviceURL = serviceURL; } /** * Set whether the authenticator saves/restores the request * during form authentication * @param saveRestoreRequest */ public void setSaveRestoreRequest(boolean saveRestoreRequest) { this.saveRestoreRequest = saveRestoreRequest; } /** * Set the {@link SAMLConfigurationProvider} fqn * @param cp fqn of a {@link SAMLConfigurationProvider} */ public void setConfigProvider(String cp) { if (cp == null) throw new IllegalStateException(ErrorCodes.NULL_ARGUMENT + cp); Class<?> clazz = SecurityActions.loadClass(getClass(), cp); if (clazz == null) throw new RuntimeException(ErrorCodes.CLASS_NOT_LOADED + cp); try { configProvider = (SAMLConfigurationProvider) clazz.newInstance(); } catch (Exception e) { throw new RuntimeException(ErrorCodes.CANNOT_CREATE_INSTANCE + cp + ":" + e.getMessage()); } } /** * Set an instance of the {@link SAMLConfigurationProvider} * @param configProvider */ public void setConfigProvider(SAMLConfigurationProvider configProvider) { this.configProvider = configProvider; } /** * Get the {@link SPType} * @return */ public SPType getConfiguration() { return spConfiguration; } /** * Set a separate issuer id * * @param issuerID */ public void setIssuerID(String issuerID) { this.issuerID = issuerID; } /** * Set the logout page * @param logOutPage */ public void setLogOutPage(String logOutPage) { logger.warn("Option logOutPage is now configured with the PicketLinkSP element."); } /** * Perform validation os the request object * * @param request * @return * @throws IOException * @throws GeneralSecurityException */ protected boolean validate(Request request) { return request.getParameter("SAMLResponse") != null; } /** * Get the Identity URL * * @return */ public String getIdentityURL() { return identityURL; } /** * Get the {@link X509Certificate} of the IDP if provided via the IDP metadata file * * @return {@link X509Certificate} or null */ public X509Certificate getIdpCertificate() { return idpCertificate; } /** * This method is a hack!!! Tomcat on account of Servlet3 changed their authenticator method signatures We utilize Java * Reflection to identify the super register method on the first call and save it. Subsquent invocations utilize the saved * {@link Method} * * @see org.apache.catalina.authenticator.AuthenticatorBase#register(org.apache.catalina.connector.Request, * org.apache.catalina.connector.Response, java.security.Principal, java.lang.String, java.lang.String, * java.lang.String) */ @Override protected void register(Request request, Response response, Principal principal, String arg3, String arg4, String arg5) { // Try the JBossAS6 version if (theSuperRegisterMethod == null && seekSuperRegisterMethod) { Class<?>[] args = new Class[] { Request.class, HttpServletResponse.class, Principal.class, String.class, String.class, String.class }; Class<?> superClass = getAuthenticatorBaseClass(); theSuperRegisterMethod = SecurityActions.getMethod(superClass, "register", args); } try { if (theSuperRegisterMethod != null) { Object[] callArgs = new Object[] { request, response, principal, arg3, arg4, arg5 }; theSuperRegisterMethod.invoke(this, callArgs); } } catch (Exception e) { throw new RuntimeException(e); } // Try the older version if (theSuperRegisterMethod == null) { seekSuperRegisterMethod = false; // Don't try to seek super register method on next invocation super.register(request, response, principal, arg3, arg4, arg5); return; } } /** * Fall back on local authentication at the service provider side * * @param request * @param response * @param loginConfig * @return * @throws IOException */ protected boolean localAuthentication(Request request, Response response, LoginConfig loginConfig) throws IOException { if (request.getUserPrincipal() == null) { logger.samlSPFallingBackToLocalFormAuthentication();// fallback try { return super.authenticate(request, response, loginConfig); } catch (NoSuchMethodError e) { // Use Reflection try { Method method = super.getClass().getMethod("authenticate", new Class[] { HttpServletRequest.class, HttpServletResponse.class, LoginConfig.class }); return (Boolean) method.invoke(this, new Object[] { request.getRequest(), response.getResponse(), loginConfig }); } catch (Exception ex) { throw logger.unableLocalAuthentication(ex); } } } else return true; } /** * Return the SAML Binding that this authenticator supports * * @see {@link JBossSAMLURIConstants#SAML_HTTP_POST_BINDING} * @see {@link JBossSAMLURIConstants#SAML_HTTP_REDIRECT_BINDING} * @return */ protected abstract String getBinding(); /** * Attempt to process a metadata file available locally */ protected void processIDPMetadataFile(String idpMetadataFile) { ServletContext servletContext = context.getServletContext(); InputStream is = servletContext.getResourceAsStream(idpMetadataFile); if (is == null) return; Object metadata = null; try { Document samlDocument = DocumentUtil.getDocument(is); SAMLParser parser = new SAMLParser(); metadata = parser.parse(DocumentUtil.getNodeAsStream(samlDocument)); } catch (Exception e) { throw new RuntimeException(e); } IDPSSODescriptorType idpSSO = null; if (metadata instanceof EntitiesDescriptorType) { EntitiesDescriptorType entities = (EntitiesDescriptorType) metadata; idpSSO = handleMetadata(entities); } else { idpSSO = handleMetadata((EntityDescriptorType) metadata); } if (idpSSO == null) { logger.samlSPUnableToGetIDPDescriptorFromMetadata(); return; } List<EndpointType> endpoints = idpSSO.getSingleSignOnService(); for (EndpointType endpoint : endpoints) { String endpointBinding = endpoint.getBinding().toString(); if (endpointBinding.contains("HTTP-POST")) endpointBinding = "POST"; else if (endpointBinding.contains("HTTP-Redirect")) endpointBinding = "REDIRECT"; if (getBinding().equals(endpointBinding)) { identityURL = endpoint.getLocation().toString(); break; } } List<KeyDescriptorType> keyDescriptors = idpSSO.getKeyDescriptor(); if (keyDescriptors.size() > 0) { this.idpCertificate = MetaDataExtractor.getCertificate(keyDescriptors.get(0)); } } /** * Process the configuration from the configuration file */ @SuppressWarnings("deprecation") protected void processConfiguration() { ServletContext servletContext = context.getServletContext(); InputStream is = servletContext.getResourceAsStream(configFile); try { // Work on the IDP Configuration if (configProvider != null) { try { if (is == null) { // Try the older version is = servletContext.getResourceAsStream(GeneralConstants.DEPRECATED_CONFIG_FILE_LOCATION); // Additionally parse the deprecated config file if (is != null && configProvider instanceof AbstractSAMLConfigurationProvider) { ((AbstractSAMLConfigurationProvider) configProvider).setConfigFile(is); } } else { // Additionally parse the consolidated config file if (is != null && configProvider instanceof AbstractSAMLConfigurationProvider) { ((AbstractSAMLConfigurationProvider) configProvider).setConsolidatedConfigFile(is); } } picketLinkConfiguration = configProvider.getPicketLinkConfiguration(); spConfiguration = configProvider.getSPConfiguration(); } catch (ProcessingException e) { throw logger.samlSPConfigurationError(e); } catch (ParsingException e) { throw logger.samlSPConfigurationError(e); } } else { if (is != null) { try { picketLinkConfiguration = ConfigurationUtil.getConfiguration(is); spConfiguration = (SPType) picketLinkConfiguration.getIdpOrSP(); } catch (ParsingException e) { logger.trace(e); throw logger.samlSPConfigurationError(e); } } else { is = servletContext.getResourceAsStream(GeneralConstants.DEPRECATED_CONFIG_FILE_LOCATION); if (is == null) throw logger.configurationFileMissing(configFile); spConfiguration = ConfigurationUtil.getSPConfiguration(is); } } if (this.picketLinkConfiguration != null) { enableAudit = picketLinkConfiguration.isEnableAudit(); //See if we have the system property enabled if(!enableAudit){ String sysProp = SecurityActions.getSystemProperty(GeneralConstants.AUDIT_ENABLE, "NULL"); if(!"NULL".equals(sysProp)){ enableAudit = Boolean.parseBoolean(sysProp); } } if (enableAudit) { if (auditHelper == null) { String securityDomainName = PicketLinkAuditHelper.getSecurityDomainName(servletContext); auditHelper = new PicketLinkAuditHelper(securityDomainName); } } } if (StringUtil.isNotNull(spConfiguration.getIdpMetadataFile())) { processIDPMetadataFile(spConfiguration.getIdpMetadataFile()); } else { this.identityURL = spConfiguration.getIdentityURL(); } this.serviceURL = spConfiguration.getServiceURL(); this.canonicalizationMethod = spConfiguration.getCanonicalizationMethod(); logger.samlSPSettingCanonicalizationMethod(canonicalizationMethod); XMLSignatureUtil.setCanonicalizationMethodType(canonicalizationMethod); logger.trace("Identity Provider URL=" + this.identityURL); } catch (Exception e) { throw new RuntimeException(e); } } protected IDPSSODescriptorType handleMetadata(EntitiesDescriptorType entities) { IDPSSODescriptorType idpSSO = null; List<Object> entityDescs = entities.getEntityDescriptor(); for (Object entityDescriptor : entityDescs) { if (entityDescriptor instanceof EntitiesDescriptorType) { idpSSO = getIDPSSODescriptor(entities); } else idpSSO = handleMetadata((EntityDescriptorType) entityDescriptor); if (idpSSO != null) break; } return idpSSO; } protected IDPSSODescriptorType handleMetadata(EntityDescriptorType entityDescriptor) { return CoreConfigUtil.getIDPDescriptor(entityDescriptor); } protected IDPSSODescriptorType getIDPSSODescriptor(EntitiesDescriptorType entities) { List<Object> entityDescs = entities.getEntityDescriptor(); for (Object entityDescriptor : entityDescs) { if (entityDescriptor instanceof EntitiesDescriptorType) { return getIDPSSODescriptor((EntitiesDescriptorType) entityDescriptor); } return CoreConfigUtil.getIDPDescriptor((EntityDescriptorType) entityDescriptor); } return null; } protected void initializeHandlerChain() throws ConfigurationException, ProcessingException { populateChainConfig(); SAML2HandlerChainConfig handlerChainConfig = new DefaultSAML2HandlerChainConfig(chainConfigOptions); Set<SAML2Handler> samlHandlers = chain.handlers(); for (SAML2Handler handler : samlHandlers) { handler.initChainConfig(handlerChainConfig); } } protected void populateChainConfig() throws ConfigurationException, ProcessingException { chainConfigOptions.put(GeneralConstants.CONFIGURATION, spConfiguration); chainConfigOptions.put(GeneralConstants.ROLE_VALIDATOR_IGNORE, "false"); // No validator as tomcat realm does validn if (doSupportSignature()) { chainConfigOptions.put(GeneralConstants.KEYPAIR, keyManager.getSigningKeyPair()); //If there is a need for X509Data in signedinfo String certificateAlias = (String)keyManager.getAdditionalOption(GeneralConstants.X509CERTIFICATE); if(certificateAlias != null){ chainConfigOptions.put(GeneralConstants.X509CERTIFICATE, keyManager.getCertificate(certificateAlias)); } } } protected void sendToLogoutPage(Request request, Response response, Session session) throws IOException, ServletException { // we are invalidated. RequestDispatcher dispatch = context.getServletContext().getRequestDispatcher(this.getConfiguration().getLogOutPage()); if (dispatch == null) logger.samlSPCouldNotDispatchToLogoutPage(this.getConfiguration().getLogOutPage()); else { logger.trace("Forwarding request to logOutPage: " + this.getConfiguration().getLogOutPage()); session.expire(); try { dispatch.forward(request, response); } catch (Exception e) { // JBAS5.1 and 6 quirkiness dispatch.forward(request.getRequest(), response); } } } // Mock test purpose public void testStart() throws LifecycleException { this.saveRestoreRequest = false; if (context == null) throw new RuntimeException("Catalina Context not set up"); startPicketLink(); } protected void startPicketLink() throws LifecycleException { SystemPropertiesUtil.ensure(); Handlers handlers = null; // Get the chain from config if (StringUtil.isNullOrEmpty(samlHandlerChainClass)) { chain = SAML2HandlerChainFactory.createChain(); } else { try { chain = SAML2HandlerChainFactory.createChain(this.samlHandlerChainClass); } catch (ProcessingException e1) { throw new LifecycleException(e1); } } ServletContext servletContext = context.getServletContext(); this.processConfiguration(); try { if (picketLinkConfiguration != null) { handlers = picketLinkConfiguration.getHandlers(); } else { // Get the handlers String handlerConfigFileName = GeneralConstants.HANDLER_CONFIG_FILE_LOCATION; handlers = ConfigurationUtil.getHandlers(servletContext.getResourceAsStream(handlerConfigFileName)); } chain.addAll(HandlerUtil.getHandlers(handlers)); this.initKeyProvider(context); this.populateChainConfig(); this.initializeHandlerChain(); } catch (Exception e) { throw new RuntimeException(e); } } /** * <p> * Indicates if digital signatures/validation of SAML assertions are enabled. Subclasses that supports signature should * override this method. * </p> * * @return */ protected boolean doSupportSignature() { if (spConfiguration != null) { return spConfiguration.isSupportsSignature(); } return false; } private Class<?> getAuthenticatorBaseClass() { Class<?> myClass = getClass(); do { myClass = myClass.getSuperclass(); } while (myClass != AuthenticatorBase.class); return myClass; } protected abstract void initKeyProvider(Context context) throws LifecycleException; public void setAuditHelper(PicketLinkAuditHelper auditHelper) { this.auditHelper = auditHelper; } }