From 2c97583f27dc2c555124e3f9aa5275632d1ee3c7 Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Fri, 23 Apr 2004 08:57:43 +0000 Subject: [PATCH] Filter to ensure web requests are received over a suitable secure channel. --- build.xml | 4 + .../securechannel/ChannelDecisionManager.java | 38 +++++ .../ChannelDecisionManagerImpl.java | 83 +++++++++ .../securechannel/ChannelEntryPoint.java | 49 ++++++ .../ChannelProcessingFilter.java | 152 +++++++++++++++++ .../RetryWithHttpsEntryPoint.java | 158 ++++++++++++++++++ .../SecureChannelRequiredException.java | 50 ++++++ .../acegisecurity/securechannel/package.html | 6 + .../etc/filter/applicationContext.xml | 19 +++ samples/contacts/etc/filter/web.xml | 14 ++ 10 files changed, 573 insertions(+) create mode 100644 core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java create mode 100644 core/src/main/java/org/acegisecurity/securechannel/package.html diff --git a/build.xml b/build.xml index 65e61e8c69..c8361478e3 100644 --- a/build.xml +++ b/build.xml @@ -183,6 +183,7 @@ + @@ -204,6 +205,7 @@ + @@ -224,6 +226,7 @@ + @@ -244,6 +247,7 @@ + diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java new file mode 100644 index 0000000000..2b01df8f68 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java @@ -0,0 +1,38 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.securechannel; + +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.intercept.web.FilterInvocation; + + +/** + * Decides whether a web channel provides sufficient security. + * + * @author Ben Alex + * @version $Id$ + */ +public interface ChannelDecisionManager { + //~ Methods ================================================================ + + /** + * Decided whether the presented {@link FilterInvocation} provides + * sufficient security based on the requested {@link + * ConfigAttributeDefinition}. + */ + public void decide(FilterInvocation invocation, + ConfigAttributeDefinition config) throws SecureChannelRequiredException; +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java new file mode 100644 index 0000000000..749ddcc50c --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java @@ -0,0 +1,83 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.securechannel; + +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.intercept.web.FilterInvocation; + +import org.springframework.beans.factory.InitializingBean; + +import java.util.Iterator; + + +/** + *

+ * Requires a secure channel for a web request if a {@link + * ConfigAttribute#getAttribute()} keyword is detected. + *

+ * + *

+ * The default keyword string is REQUIRES_SECURE_CHANNEL, but this + * may be overriden to any value. The ConfigAttribute must + * exactly match the case of the keyword string. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class ChannelDecisionManagerImpl implements InitializingBean, + ChannelDecisionManager { + //~ Instance fields ======================================================== + + private String keyword = "REQUIRES_SECURE_CHANNEL"; + + //~ Methods ================================================================ + + public void setKeyword(String keyword) { + this.keyword = keyword; + } + + public String getKeyword() { + return keyword; + } + + public void afterPropertiesSet() throws Exception { + if ((keyword == null) || "".equals(keyword)) { + throw new IllegalArgumentException("keyword required"); + } + } + + public void decide(FilterInvocation invocation, + ConfigAttributeDefinition config) throws SecureChannelRequiredException { + if ((invocation == null) || (config == null)) { + throw new IllegalArgumentException("Nulls cannot be provided"); + } + + Iterator iter = config.getConfigAttributes(); + + while (iter.hasNext()) { + ConfigAttribute attribute = (ConfigAttribute) iter.next(); + + if (attribute.equals(keyword)) { + if (!invocation.getHttpRequest().isSecure()) { + throw new SecureChannelRequiredException( + "Request is not being made over a secure channel"); + } + } + } + } +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java new file mode 100644 index 0000000000..0187b2e822 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java @@ -0,0 +1,49 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.securechannel; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + + +/** + * Used by {@link ChannelProcessingFilter} to launch a secure web channel. + * + * @author Ben Alex + * @version $Id$ + */ +public interface ChannelEntryPoint { + //~ Methods ================================================================ + + /** + * Commences a secure channel. + * + *

+ * Implementations should modify the headers on the + * ServletResponse as necessary to commence the user agent + * using the secure channel. + *

+ * + * @param request that resulted in a + * SecureChannelRequiredException + * @param response so that the user agent can begin using a secure channel + */ + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException; +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java new file mode 100644 index 0000000000..3e33b9e80f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java @@ -0,0 +1,152 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.securechannel; + +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.intercept.web.FilterInvocation; +import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Ensures a request is delivered over a secure channel. + * + *

+ * Internally uses a {@link FilterInvocation} to represent the request, so that + * the FilterInvocation-related property editors and lookup + * classes can be used. + *

+ * + *

+ * Do not use this class directly. Instead configure + * web.xml to use the {@link + * net.sf.acegisecurity.util.FilterToBeanProxy}. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class ChannelProcessingFilter implements InitializingBean, Filter { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(ChannelProcessingFilter.class); + + //~ Instance fields ======================================================== + + private ChannelDecisionManager channelDecisionManager; + private ChannelEntryPoint channelEntryPoint; + private FilterInvocationDefinitionSource filterInvocationDefinitionSource; + + //~ Methods ================================================================ + + public void setChannelDecisionManager( + ChannelDecisionManager channelDecisionManager) { + this.channelDecisionManager = channelDecisionManager; + } + + public ChannelDecisionManager getChannelDecisionManager() { + return channelDecisionManager; + } + + public void setChannelEntryPoint(ChannelEntryPoint channelEntryPoint) { + this.channelEntryPoint = channelEntryPoint; + } + + public ChannelEntryPoint getChannelEntryPoint() { + return channelEntryPoint; + } + + public void setFilterInvocationDefinitionSource( + FilterInvocationDefinitionSource filterInvocationDefinitionSource) { + this.filterInvocationDefinitionSource = filterInvocationDefinitionSource; + } + + public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() { + return filterInvocationDefinitionSource; + } + + public void afterPropertiesSet() throws Exception { + if (filterInvocationDefinitionSource == null) { + throw new IllegalArgumentException( + "filterInvocationDefinitionSource must be specified"); + } + + if (channelDecisionManager == null) { + throw new IllegalArgumentException( + "channelDecisionManager must be specified"); + } + + if (channelEntryPoint == null) { + throw new IllegalArgumentException( + "channelEntryPoint must be specified"); + } + } + + public void destroy() {} + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException("HttpServletRequest required"); + } + + if (!(response instanceof HttpServletResponse)) { + throw new ServletException("HttpServletResponse required"); + } + + FilterInvocation fi = new FilterInvocation(request, response, chain); + ConfigAttributeDefinition attr = this.filterInvocationDefinitionSource + .getAttributes(fi); + + if (attr != null) { + if (logger.isDebugEnabled()) { + logger.debug("Request : " + request.toString() + + "; ConfigAttributes: " + attr.toString()); + } + + try { + channelDecisionManager.decide(fi, attr); + } catch (SecureChannelRequiredException channelException) { + if (logger.isDebugEnabled()) { + logger.debug("Channel insufficient (" + + channelException.getMessage() + + "); delegating to channelEntryPoint"); + } + + channelEntryPoint.commence(request, response); + } + } + + chain.doFilter(request, response); + } + + public void init(FilterConfig filterConfig) throws ServletException {} +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java new file mode 100644 index 0000000000..27c5bb5fa1 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java @@ -0,0 +1,158 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.securechannel; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +import java.io.IOException; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Commences a secure channel by retrying the original request using HTTPS. + * + *

+ * This entry point should suffice in most circumstances. However, it is not + * intended to properly handle HTTP POSTs or other usage where a standard + * redirect would cause an issue. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class RetryWithHttpsEntryPoint implements InitializingBean, + ChannelEntryPoint { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(RetryWithHttpsEntryPoint.class); + + //~ Instance fields ======================================================== + + private Map httpsPortMappings; + + //~ Constructors =========================================================== + + public RetryWithHttpsEntryPoint() { + httpsPortMappings = new HashMap(); + httpsPortMappings.put(new Integer(80), new Integer(443)); + httpsPortMappings.put(new Integer(8080), new Integer(8443)); + } + + //~ Methods ================================================================ + + /** + *

+ * Set to override the default http port to https port mappings of 80:443, + * and 8080:8443. + *

+ * In a Spring XML ApplicationContext, a definition would look something + * like this: + *
+     *   <property name="httpsPortMapping">
+     *     <map>
+     *       <entry key="80"><value>443</value></entry>
+     *       <entry key="8080"><value>8443</value></entry>
+     *     </map>
+     *   </property>
+     * 
+ * + * @param newMappings A Map consisting of String keys and String values, + * where for each entry the key is the string representation of an + * integer http port number, and the value is the string + * representation of the corresponding integer https port number. + * + * @throws IllegalArgumentException if input map does not consist of String + * keys and values, each representing an integer port number in + * the range 1-65535 for that mapping. + */ + public void setHttpsPortMappings(HashMap newMappings) { + httpsPortMappings.clear(); + + Iterator it = newMappings.entrySet().iterator(); + + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + Integer httpPort = new Integer((String) entry.getKey()); + Integer httpsPort = new Integer((String) entry.getValue()); + + if ((httpPort.intValue() < 1) || (httpPort.intValue() > 65535) + || (httpsPort.intValue() < 1) || (httpsPort.intValue() > 65535)) { + throw new IllegalArgumentException( + "one or both ports out of legal range: " + httpPort + ", " + + httpsPort); + } + + httpsPortMappings.put(httpPort, httpsPort); + + if (httpsPortMappings.size() < 1) { + throw new IllegalArgumentException("must map at least one port"); + } + } + } + + public void afterPropertiesSet() throws Exception { + if (httpsPortMappings == null) { + throw new IllegalArgumentException("httpsPortMappings required"); + } + } + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + + String pathInfo = req.getPathInfo(); + String queryString = req.getQueryString(); + String contextPath = req.getContextPath(); + String destination = req.getServletPath() + + ((pathInfo == null) ? "" : pathInfo) + + ((queryString == null) ? "" : ("?" + queryString)); + + String redirectUrl = contextPath; + + Integer httpPort = new Integer(req.getServerPort()); + Integer httpsPort = (Integer) httpsPortMappings.get(httpPort); + + if (httpsPort != null) { + String serverName = req.getServerName(); + redirectUrl = "https://" + serverName + ":" + httpsPort + + contextPath + destination; + } + + ((HttpServletResponse) response).sendRedirect(redirectUrl); + } + + /** + * Returns the translated (Integer -> Integer) version of the original port + * mapping specified via setHttpsPortMapping() + * + * @return DOCUMENT ME! + */ + protected Map getTranslatedHttpsPortMappings() { + return httpsPortMappings; + } +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java b/core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java new file mode 100644 index 0000000000..62ed5410b6 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java @@ -0,0 +1,50 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.securechannel; + +import net.sf.acegisecurity.AccessDeniedException; + + +/** + * Thrown if a secure web channel is required, but is not present. + * + * @author Ben Alex + * @version $Id$ + */ +public class SecureChannelRequiredException extends AccessDeniedException { + //~ Constructors =========================================================== + + /** + * Constructs a SecureChannelRequiredException with the + * specified message. + * + * @param msg the detail message. + */ + public SecureChannelRequiredException(String msg) { + super(msg); + } + + /** + * Constructs a SecureChannelRequiredException with the + * specified message and root cause. + * + * @param msg the detail message. + * @param t root cause + */ + public SecureChannelRequiredException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/core/src/main/java/org/acegisecurity/securechannel/package.html b/core/src/main/java/org/acegisecurity/securechannel/package.html new file mode 100644 index 0000000000..257fc29113 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/securechannel/package.html @@ -0,0 +1,6 @@ + + +Classes that ensure web requests are received over required +transport channels. + + diff --git a/samples/contacts/etc/filter/applicationContext.xml b/samples/contacts/etc/filter/applicationContext.xml index ecc2831420..a89199cdc8 100644 --- a/samples/contacts/etc/filter/applicationContext.xml +++ b/samples/contacts/etc/filter/applicationContext.xml @@ -133,6 +133,25 @@ + + + + + + + + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + \A/secure/.*\Z=REQUIRES_SECURE_CHANNEL + \A/info/.*\Z=REQUIRES_SECURE_CHANNEL + \A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL + + + + + + + + diff --git a/samples/contacts/etc/filter/web.xml b/samples/contacts/etc/filter/web.xml index df4ef191cc..86ae3db0f4 100644 --- a/samples/contacts/etc/filter/web.xml +++ b/samples/contacts/etc/filter/web.xml @@ -24,6 +24,15 @@ /WEB-INF/applicationContext.xml + + Acegi Channel Processing Filter + net.sf.acegisecurity.util.FilterToBeanProxy + + targetClass + net.sf.acegisecurity.securechannel.ChannelProcessingFilter + + + Acegi Authentication Processing Filter net.sf.acegisecurity.util.FilterToBeanProxy @@ -56,6 +65,11 @@ + + Acegi Channel Processing Filter + /* + + Acegi Authentication Processing Filter /*