/*
* 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 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.common.ErrorCodes;
import org.picketlink.common.PicketLinkLogger;
import org.picketlink.common.PicketLinkLoggerFactory;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ParsingException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.util.DocumentUtil;
import org.picketlink.common.util.SystemPropertiesUtil;
import org.picketlink.config.federation.PicketLinkType;
import org.picketlink.config.federation.SPType;
import org.picketlink.config.federation.handler.Handlers;
import org.picketlink.identity.federation.api.saml.v2.metadata.MetaDataExtractor;
import org.picketlink.identity.federation.core.audit.PicketLinkAuditHelper;
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.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.HandlerUtil;
import org.picketlink.identity.federation.core.util.CoreConfigUtil;
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.core.SessionManager;
import org.picketlink.identity.federation.web.util.ConfigurationUtil;
import org.picketlink.identity.federation.web.util.SAMLConfigurationProvider;
import org.w3c.dom.Document;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSessionListener;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
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.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.picketlink.common.constants.GeneralConstants.CONFIG_FILE_LOCATION;
import static org.picketlink.common.util.StringUtil.isNotNull;
import static org.picketlink.common.util.StringUtil.isNullOrEmpty;
/**
* 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 volatile PicketLinkAuditHelper auditHelper = null;
protected volatile TrustKeyManager keyManager;
protected volatile PicketLinkType picketLinkConfiguration = null;
protected volatile String serviceURL = null;
protected volatile String issuerID = null;
protected String configFile;
/**
* 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;
// 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 volatile 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;
protected int timerInterval = -1;
protected Timer timer = null;
protected IDPSSODescriptorType idpMetadata;
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 configProviderFQN fqn of a {@link SAMLConfigurationProvider}
*/
public void setConfigProvider(String configProviderFQN) {
if (configProviderFQN == null) {
throw logger.nullValueError("cp");
}
Class<?> clazz = SecurityActions.loadClass(getClass(), configProviderFQN);
if (clazz == null) {
throw logger.nullValueError("clazz");
}
try {
configProvider = (SAMLConfigurationProvider) clazz.newInstance();
} catch (Exception e) {
throw logger.runtimeException(ErrorCodes.CANNOT_CREATE_INSTANCE + configProviderFQN + ":" + e.getMessage(), e);
}
}
/**
* 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 (SPType) this.picketLinkConfiguration.getIdpOrSP();
}
/**
* 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.");
}
/**
* Set the Timer Value to reload the configuration
*
* @param value an integer value that represents timer value (in miliseconds)
*/
public void setTimerInterval(String value) {
if (isNotNull(value)) {
timerInterval = Integer.parseInt(value);
}
}
/**
* Perform validation os the request object
*
* @param request
*
* @return
*/
protected boolean validate(Request request) {
return request.getParameter("SAMLResponse") != null;
}
/**
* Get the Identity URL
*
* @return
*/
public String getIdentityURL() {
return getConfiguration().getIdentityURL();
}
/**
* 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
*
* @return
*
* @see {@link org.picketlink.common.constants.JBossSAMLURIConstants#SAML_HTTP_POST_BINDING}
* @see {@link org.picketlink.common.constants.JBossSAMLURIConstants#SAML_HTTP_REDIRECT_BINDING}
*/
protected abstract String getBinding();
/**
* Attempt to process a metadata file available locally
* @param configuration
*/
protected IDPSSODescriptorType getIdpMetadataFromFile(SPType configuration) {
ServletContext servletContext = context.getServletContext();
InputStream is = servletContext.getResourceAsStream(configuration.getIdpMetadataFile());
if (is == null) {
return null;
}
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 idpSSO;
}
return idpSSO;
}
/**
* Process the configuration from the configuration file
*/
@SuppressWarnings("deprecation")
protected void processConfiguration() {
ServletContext servletContext = context.getServletContext();
InputStream is;
if (isNullOrEmpty(this.configFile)) {
is = servletContext.getResourceAsStream(CONFIG_FILE_LOCATION);
} else {
try {
is = new FileInputStream(this.configFile);
} catch (FileNotFoundException e) {
throw logger.samlIDPConfigurationError(e);
}
}
PicketLinkType picketLinkType;
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);
}
}
picketLinkType = configProvider.getPicketLinkConfiguration();
picketLinkType.setIdpOrSP(configProvider.getSPConfiguration());
} catch (ProcessingException e) {
throw logger.samlSPConfigurationError(e);
} catch (ParsingException e) {
throw logger.samlSPConfigurationError(e);
}
} else {
if (is != null) {
try {
picketLinkType = ConfigurationUtil.getConfiguration(is);
} 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);
}
picketLinkType = new PicketLinkType();
picketLinkType.setIdpOrSP(ConfigurationUtil.getSPConfiguration(is));
}
}
//Close the InputStream as we no longer need it
if(is != null){
try {
is.close();
} catch (IOException e) {
//ignore
}
}
Boolean enableAudit = picketLinkType.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);
}
}
SPType spConfiguration = (SPType) picketLinkType.getIdpOrSP();
processIdPMetadata(spConfiguration);
this.serviceURL = spConfiguration.getServiceURL();
this.canonicalizationMethod = spConfiguration.getCanonicalizationMethod();
this.picketLinkConfiguration = picketLinkType;
logger.samlSPSettingCanonicalizationMethod(canonicalizationMethod);
XMLSignatureUtil.setCanonicalizationMethodType(canonicalizationMethod);
try {
this.initKeyProvider(context);
this.initializeHandlerChain(picketLinkType);
} catch (Exception e) {
throw new RuntimeException(e);
}
logger.trace("Identity Provider URL=" + getConfiguration().getIdentityURL());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void processIdPMetadata(SPType spConfiguration) {
IDPSSODescriptorType idpssoDescriptorType = null;
if (isNotNull(spConfiguration.getIdpMetadataFile())) {
idpssoDescriptorType = getIdpMetadataFromFile(spConfiguration);
} else {
idpssoDescriptorType = getIdpMetadataFromProvider(spConfiguration);
}
if (idpssoDescriptorType != null) {
List<EndpointType> endpoints = idpssoDescriptorType.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 (spConfiguration.getBindingType().equals(endpointBinding)) {
spConfiguration.setIdentityURL(endpoint.getLocation().toString());
break;
}
}
List<KeyDescriptorType> keyDescriptors = idpssoDescriptorType.getKeyDescriptor();
if (keyDescriptors.size() > 0) {
this.idpCertificate = MetaDataExtractor.getCertificate(keyDescriptors.get(0));
}
this.idpMetadata = idpssoDescriptorType;
}
}
private IDPSSODescriptorType getIdpMetadataFromProvider(SPType spConfiguration) {
List<EntityDescriptorType> entityDescriptors = CoreConfigUtil.getMetadataConfiguration(spConfiguration,
this.context.getServletContext());
if (entityDescriptors != null) {
for (EntityDescriptorType entityDescriptorType : entityDescriptors) {
IDPSSODescriptorType idpssoDescriptorType = handleMetadata(entityDescriptorType);
if (idpssoDescriptorType != null) {
return idpssoDescriptorType;
}
}
}
return null;
}
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(PicketLinkType picketLinkType) throws Exception {
SAML2HandlerChain handlerChain;
// Get the chain from config
if (isNullOrEmpty(samlHandlerChainClass)) {
handlerChain = SAML2HandlerChainFactory.createChain();
} else {
try {
handlerChain = SAML2HandlerChainFactory.createChain(this.samlHandlerChainClass);
} catch (ProcessingException e1) {
throw new LifecycleException(e1);
}
}
Handlers handlers = picketLinkType.getHandlers();
if (handlers == null) {
// Get the handlers
String handlerConfigFileName = GeneralConstants.HANDLER_CONFIG_FILE_LOCATION;
ServletContext servletContext = context.getServletContext();
handlers = ConfigurationUtil.getHandlers(servletContext.getResourceAsStream(handlerConfigFileName));
}
picketLinkType.setHandlers(handlers);
handlerChain.addAll(HandlerUtil.getHandlers(handlers));
populateChainConfig(picketLinkType);
SAML2HandlerChainConfig handlerChainConfig = new DefaultSAML2HandlerChainConfig(chainConfigOptions);
Set<SAML2Handler> samlHandlers = handlerChain.handlers();
for (SAML2Handler handler : samlHandlers) {
handler.initChainConfig(handlerChainConfig);
}
chain = handlerChain;
}
protected void populateChainConfig(PicketLinkType picketLinkType) throws ConfigurationException, ProcessingException {
Map<String, Object> chainConfigOptions = new HashMap<String, Object>();
chainConfigOptions.put(GeneralConstants.CONFIGURATION, picketLinkType.getIdpOrSP());
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));
}
}
this.chainConfigOptions = chainConfigOptions;
}
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;
//Introduce a timer to reload configuration if desired
if (timerInterval > 0) {
if (timer == null) {
timer = new Timer();
}
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
logger.info("Reloading configuration for " + context.getName());
processConfiguration();
}
}, timerInterval, timerInterval);
}
ServletContext servletContext = context.getServletContext();
this.processConfiguration();
new SessionManager(servletContext, new SessionManager.InitializationCallback() {
@Override
public void registerSessionListener(Class<? extends HttpSessionListener> listener) {
context.addApplicationListener(listener.getName());
}
});
}
protected void stopPicketLink() {
if (timer != null) {
timer.cancel();
}
}
/**
* <p> Indicates if digital signatures/validation of SAML assertions are enabled. Subclasses that supports signature should
* override this method. </p>
*
* @return
*/
protected boolean doSupportSignature() {
return getConfiguration().isSupportsSignature();
}
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;
}
}