Enhance AuthenticationProcessingFilterEntryPoint and related classes, to support a property forcing the login page to be access via https even if the original intercepted request came in as http.

This commit is contained in:
Colin Sampaleanu 2004-04-22 21:47:05 +00:00
parent 088563c363
commit e2de3c9dbc
16 changed files with 286 additions and 128 deletions

View File

@ -320,21 +320,22 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean {
logger.debug("Switching to RunAs Authentication: "
+ runAs.toString());
}
SecureContext origSecureContext = null;
try {
origSecureContext = (SecureContext) ContextHolder.getContext();
origSecureContext = (SecureContext) ContextHolder
.getContext();
context.setAuthentication(runAs);
ContextHolder.setContext((Context) context);
return callback.proceedWithObject(object);
}
finally {
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Reverting to original Authentication: "
+ authenticated.toString());
}
origSecureContext.setAuthentication(authenticated);
ContextHolder.setContext(origSecureContext);
}

View File

@ -37,6 +37,7 @@ import javax.servlet.http.HttpServletResponse;
* </p>
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public class FilterInvocation {
@ -80,6 +81,13 @@ public class FilterInvocation {
return chain;
}
public String getFullRequestUrl() {
return getHttpRequest().getRequestURL().toString()
+ ((getHttpRequest().getQueryString() == null) ? ""
: ("?"
+ getHttpRequest().getQueryString()));
}
public HttpServletRequest getHttpRequest() {
return (HttpServletRequest) request;
}

View File

@ -154,11 +154,11 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean {
if (logger.isDebugEnabled()) {
logger.debug(
"Authentication failed - adding target URL to Session: "
+ fi.getRequestUrl());
+ fi.getFullRequestUrl());
}
((HttpServletRequest) request).getSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY,
fi.getRequestUrl());
fi.getFullRequestUrl());
authenticationEntryPoint.commence(request, response);
} catch (AccessDeniedException accessDenied) {
if (logger.isDebugEnabled()) {

View File

@ -155,7 +155,8 @@ public class CasAuthenticationProvider implements AuthenticationProvider,
}
// Ensure credentials are presented
if (authentication.getCredentials() == null || "".equals(authentication.getCredentials())) {
if ((authentication.getCredentials() == null)
|| "".equals(authentication.getCredentials())) {
throw new BadCredentialsException(
"Failed to provide a CAS service ticket to validate");
}

View File

@ -19,6 +19,7 @@ import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.providers.AbstractAuthenticationToken;
import java.io.Serializable;
import java.util.List;
@ -28,7 +29,8 @@ import java.util.List;
* @author Ben Alex
* @version $Id$
*/
public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
public class CasAuthenticationToken extends AbstractAuthenticationToken
implements Serializable {
//~ Instance fields ========================================================
private List proxyList;

View File

@ -63,7 +63,9 @@ import javax.servlet.http.HttpServletResponse;
* <code>defaultTargetUrl</code> indicates the URL that should be used for
* redirection if the <code>HttpSession</code> attribute named {@link
* #ACEGI_SECURITY_TARGET_URL_KEY} does not indicate the target URL once
* authentication is completed successfully. eg: <code>/</code>.
* authentication is completed successfully. eg: <code>/</code>. This will be
* treated as relative to the web-app's context path, and should include the
* leading <code>/</code>.
* </li>
* <li>
* <code>authenticationFailureUrl</code> indicates the URL that should be used
@ -78,6 +80,7 @@ import javax.servlet.http.HttpServletResponse;
*
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public abstract class AbstractProcessingFilter implements Filter,
@ -240,7 +243,7 @@ public abstract class AbstractProcessingFilter implements Filter,
null);
if (targetUrl == null) {
targetUrl = defaultTargetUrl;
targetUrl = httpRequest.getContextPath() + defaultTargetUrl;
}
if (logger.isDebugEnabled()) {
@ -249,7 +252,7 @@ public abstract class AbstractProcessingFilter implements Filter,
+ targetUrl);
}
httpResponse.sendRedirect(httpRequest.getContextPath() + targetUrl);
httpResponse.sendRedirect(targetUrl);
return;
}

View File

@ -97,7 +97,7 @@ public class CasProcessingFilter extends AbstractProcessingFilter {
String username = CAS_STATEFUL_IDENTIFIER;
String password = request.getParameter("ticket");
if (password == null) {
if (password == null) {
password = "";
}

View File

@ -35,8 +35,7 @@ public class ServiceProperties implements InitializingBean {
//~ Instance fields ========================================================
private String service;
private boolean sendRenew = false;
private boolean sendRenew = false;
//~ Methods ================================================================
@ -47,12 +46,15 @@ public class ServiceProperties implements InitializingBean {
/**
* Indicates whether the <code>renew</code> parameter should be sent to the
* CAS login URL and CAS validation URL.
* <P> If <code>true</code>, it will
* force CAS to authenticate the user again (even if the user has
* previously authenticated). During ticket validation it will require the
* ticket was generated as a consequence of an explicit login. High
* security applications would probably set this to <code>true</code>.
* Defaults to <code>false</code>, providing automated single sign on.
*
* <p>
* If <code>true</code>, it will force CAS to authenticate the user again
* (even if the user has previously authenticated). During ticket
* validation it will require the ticket was generated as a consequence of
* an explicit login. High security applications would probably set this
* to <code>true</code>. Defaults to <code>false</code>, providing
* automated single sign on.
* </p>
*
* @return whether to send the <code>renew</code> parameter to CAS
*/
@ -65,13 +67,14 @@ public class ServiceProperties implements InitializingBean {
}
/**
* Represents the service the user is authenticating to.
*
* <B>This service is the callback URL
* belonging to the local Acegi Security System for Spring secured
* application. For example,
* Represents the service the user is authenticating to.
*
* <p>
* This service is the callback URL belonging to the local Acegi Security
* System for Spring secured application. For example,
* </p>
* <code>https://www.mycompany.com/application/j_acegi_cas_security_check</code>
*
*
* @return the URL of the service the user is authenticating to
*/
public String getService() {

View File

@ -20,6 +20,7 @@ import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
import org.springframework.beans.factory.InitializingBean;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@ -32,8 +33,31 @@ import javax.servlet.http.HttpServletResponse;
/**
* <p>
* Used by the <code>SecurityEnforcementFilter</code> to commence
* authentication via the {@link AuthenticationProcessingFilter}.
* authentication via the {@link AuthenticationProcessingFilter}. This object
* holds the location of the login form, relative to the web app context path,
* and is used to commence a redirect to that form.
* </p>
*
* <p>
* By setting the <em>forceHttps</em> property to true, you may configure the
* class to force the protocol used for the login form to be
* <code>https</code>, even if the original intercepted request for a resource
* used the <code>http</code> protocol. When this happens, after a successful
* login (via https), the original resource will still be accessed as http,
* via the original request URL. For the forced https feature to work, the
* class must have a valid mapping from an http port in the original request
* to an https port for the login page (the same server name will be used,
* only the scheme and port will be changed). By default, http requests to
* port 80 will be mapped to login page https requests on port 443 (standard
* https port), and port 8080 will be mapped to port 8443. These mappings may
* be customized by setting the <em>httpsPortMappings</em> property. Any
* intercepted http request on a port which does not have a mapping will
* result in the protocol remaining as http. Any intercepted request which is
* already https will always result in the login page being accessed as https,
* regardless of the state of the <em>forceHttps</em> property.
* </p>
*
* @author Ben Alex
* @author colin sampaleanu
@ -43,24 +67,95 @@ public class AuthenticationProcessingFilterEntryPoint
implements AuthenticationEntryPoint, InitializingBean {
//~ Instance fields ========================================================
/**
* The URL where the <code>AuthenticationProcessingFilter</code> login page
* can be found.
*/
private HashMap httpsPortMappings;
private String loginFormUrl;
private boolean forceHttps = false;
private HashMap httpsPortMapping;
//~ Methods ================================================================
//~ Constructors ===========================================================
public AuthenticationProcessingFilterEntryPoint() {
httpsPortMapping = new HashMap();
httpsPortMapping.put(new Integer(80), new Integer(443));
httpsPortMapping.put(new Integer(8080), new Integer(8443));
httpsPortMappings = new HashMap();
httpsPortMappings.put(new Integer(80), new Integer(443));
httpsPortMappings.put(new Integer(8080), new Integer(8443));
}
//~ Methods ================================================================
/**
* Set to true to force login form access to be via https. If this value is
* ture (the default is false), and the incoming request for the protected
* resource which triggered the interceptor was not already
* <code>https</code>, then
*
* @param forceHttps
*
* @todo Generated comment
*/
public void setForceHttps(boolean forceHttps) {
this.forceHttps = forceHttps;
}
public boolean getForceHttps() {
return forceHttps;
}
/**
* <p>
* Set to override the default http port to https port mappings of 80:443,
* and 8080:8443.
* </p>
* In a Spring XML ApplicationContext, a definition would look something
* like this:
* <pre>
* &lt;property name="httpsPortMapping">
* &lt;map>
* &lt;entry key="80">&lt;value>443&lt;/value>&lt;/entry>
* &lt;entry key="8080">&lt;value>8443&lt;/value>&lt;/entry>
* &lt;/map>
* &lt;/property>
* </pre>
*
* @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");
}
}
}
/**
* The URL where the <code>AuthenticationProcessingFilter</code> login page
* can be found. Should be relative to the web-app context path, and
* include a leading <code>/</code>
*
* @param loginFormUrl
*/
public void setLoginFormUrl(String loginFormUrl) {
this.loginFormUrl = loginFormUrl;
}
@ -77,59 +172,32 @@ public class AuthenticationProcessingFilterEntryPoint
public void commence(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String contextPath = req.getContextPath();
String redirectUrl = contextPath + loginFormUrl;
String contextPath = req.getContextPath();
String redirectUrl = contextPath + loginFormUrl;
if (forceHttps && req.getScheme().equals("http")) {
Integer httpPort = new Integer(req.getServerPort());
Integer httpsPort = (Integer) httpsPortMapping.get(httpPort);
if (httpsPort != null ) {
Integer httpsPort = (Integer) httpsPortMappings.get(httpPort);
if (httpsPort != null) {
String serverName = req.getServerName();
redirectUrl = "https://" + serverName + ":" + httpsPort + contextPath
+ loginFormUrl;
redirectUrl = "https://" + serverName + ":" + httpsPort
+ contextPath + loginFormUrl;
}
}
((HttpServletResponse) response).sendRedirect(redirectUrl);
}
public void setForceHttps(boolean forceSsl) {
this.forceHttps = forceSsl;
}
public boolean getForceHttps() {
return forceHttps;
}
/**
* @throws IllegalArgumentException if input map does not consist of String keys
* and values, each representing an integer port number for one mapping.
*/
public void setHttpsPortMapping(HashMap newMappings) {
httpsPortMapping.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);
httpsPortMapping.put(httpPort, httpsPort);
if (httpsPortMapping.size() < 1)
throw new IllegalArgumentException("must map at least one port");
}
}
/**
* Returns the translated (Integer -> Integer) version of the original port
* mapping specified via setHttpsPortMapping()
* mapping specified via setHttpsPortMapping()
*
* @return DOCUMENT ME!
*/
protected HashMap getTranslatedHttpsPortMapping() {
return httpsPortMapping;
protected HashMap getTranslatedHttpsPortMappings() {
return httpsPortMappings;
}
}

View File

@ -15,10 +15,10 @@
package net.sf.acegisecurity.providers.dao;
import java.io.Serializable;
import net.sf.acegisecurity.GrantedAuthority;
import java.io.Serializable;
/**
* Models core user information retieved by an {@link AuthenticationDao}.

View File

@ -42,6 +42,7 @@ import javax.servlet.http.HttpSession;
* </p>
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public class MockHttpServletRequest implements HttpServletRequest {
@ -53,7 +54,11 @@ public class MockHttpServletRequest implements HttpServletRequest {
private Principal principal;
private String contextPath = "";
private String queryString = null;
private String requestURL;
private String scheme;
private String serverName;
private String servletPath;
private int serverPort;
//~ Constructors ===========================================================
@ -235,8 +240,12 @@ public class MockHttpServletRequest implements HttpServletRequest {
throw new UnsupportedOperationException("mock method not implemented");
}
public void setRequestURL(String newRequestURL) {
requestURL = newRequestURL;
}
public StringBuffer getRequestURL() {
throw new UnsupportedOperationException("mock method not implemented");
return new StringBuffer(requestURL);
}
public String getRequestedSessionId() {
@ -259,20 +268,32 @@ public class MockHttpServletRequest implements HttpServletRequest {
throw new UnsupportedOperationException("mock method not implemented");
}
public void setScheme(String newScheme) {
scheme = newScheme;
}
public String getScheme() {
throw new UnsupportedOperationException("mock method not implemented");
return scheme;
}
public boolean isSecure() {
throw new UnsupportedOperationException("mock method not implemented");
}
public void setServerName(String newServerName) {
serverName = newServerName;
}
public String getServerName() {
throw new UnsupportedOperationException("mock method not implemented");
return serverName;
}
public void setServerPort(int newPort) {
serverPort = newPort;
}
public int getServerPort() {
throw new UnsupportedOperationException("mock method not implemented");
return serverPort;
}
public void setServletPath(String servletPath) {

View File

@ -41,6 +41,7 @@ import javax.servlet.ServletResponse;
* Tests {@link FilterInvocation}.
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public class FilterInvocationTests extends TestCase {
@ -67,6 +68,7 @@ public class FilterInvocationTests extends TestCase {
public void testGettersAndStringMethods() {
MockHttpServletRequest request = new MockHttpServletRequest(null, null);
request.setServletPath("/HelloWorld");
request.setRequestURL("http://www.example.com/mycontext/HelloWorld");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
@ -78,6 +80,8 @@ public class FilterInvocationTests extends TestCase {
assertEquals(chain, fi.getChain());
assertEquals("/HelloWorld", fi.getRequestUrl());
assertEquals("FilterInvocation: URL: /HelloWorld", fi.toString());
assertEquals("http://www.example.com/mycontext/HelloWorld",
fi.getFullRequestUrl());
}
public void testNoArgsConstructor() {
@ -156,23 +160,29 @@ public class FilterInvocationTests extends TestCase {
public void testStringMethodsWithAQueryString() {
MockHttpServletRequest request = new MockHttpServletRequest("foo=bar");
request.setServletPath("/HelloWorld");
request.setRequestURL("http://www.example.com/mycontext/HelloWorld");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
FilterInvocation fi = new FilterInvocation(request, response, chain);
assertEquals("/HelloWorld?foo=bar", fi.getRequestUrl());
assertEquals("FilterInvocation: URL: /HelloWorld?foo=bar", fi.toString());
assertEquals("http://www.example.com/mycontext/HelloWorld?foo=bar",
fi.getFullRequestUrl());
}
public void testStringMethodsWithoutAnyQueryString() {
MockHttpServletRequest request = new MockHttpServletRequest(null, null);
request.setServletPath("/HelloWorld");
request.setRequestURL("http://www.example.com/mycontext/HelloWorld");
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
FilterInvocation fi = new FilterInvocation(request, response, chain);
assertEquals("/HelloWorld", fi.getRequestUrl());
assertEquals("FilterInvocation: URL: /HelloWorld", fi.toString());
assertEquals("http://www.example.com/mycontext/HelloWorld",
fi.getFullRequestUrl());
}
//~ Inner Classes ==========================================================

View File

@ -128,6 +128,8 @@ public class SecurityEnforcementFilterTests extends TestCase {
MockHttpServletRequest request = new MockHttpServletRequest(null,
new MockHttpSession());
request.setServletPath("/secure/page.html");
request.setRequestURL(
"http://www.example.com/mycontext/secure/page.html");
// Setup our expectation that the filter chain will not be invoked, as access is denied
MockFilterChain chain = new MockFilterChain(false);
@ -146,7 +148,7 @@ public class SecurityEnforcementFilterTests extends TestCase {
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, chain);
assertEquals("/login.jsp", response.getRedirect());
assertEquals("/secure/page.html",
assertEquals("http://www.example.com/mycontext/secure/page.html",
request.getSession().getAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY));
}

View File

@ -65,7 +65,7 @@ public class ServicePropertiesTests extends TestCase {
sp.setService("https://mycompany.com/service");
assertEquals("https://mycompany.com/service", sp.getService());
sp.afterPropertiesSet();
}
}

View File

@ -15,22 +15,22 @@
package net.sf.acegisecurity.ui.webapp;
import java.util.HashMap;
import junit.framework.TestCase;
import net.sf.acegisecurity.MockHttpServletRequest;
import net.sf.acegisecurity.MockHttpServletResponse;
import java.util.HashMap;
/**
* Tests {@link AuthenticationProcessingFilterEntryPoint}.
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public class AuthenticationProcessingFilterEntryPointTests extends TestCase {
//~ Methods ================================================================
public final void setUp() throws Exception {
@ -57,37 +57,48 @@ public class AuthenticationProcessingFilterEntryPointTests extends TestCase {
ep.setLoginFormUrl("/hello");
assertEquals("/hello", ep.getLoginFormUrl());
}
public void testSetSslPortMapping() {
public void testHttpsOperation() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest(
"/some_path");
request.setScheme("http");
request.setServerName("www.example.com");
request.setContextPath("/bigWebApp");
request.setServerPort(80);
MockHttpServletResponse response = new MockHttpServletResponse();
AuthenticationProcessingFilterEntryPoint ep = new AuthenticationProcessingFilterEntryPoint();
ep.setLoginFormUrl("/hello");
ep.setForceHttps(true);
ep.afterPropertiesSet();
ep.commence(request, response);
assertEquals("https://www.example.com:443/bigWebApp/hello",
response.getRedirect());
request.setServerPort(8080);
ep.commence(request, response);
assertEquals("https://www.example.com:8443/bigWebApp/hello",
response.getRedirect());
// check that unknown port leaves things as-is
request.setServerPort(8888);
ep.commence(request, response);
assertEquals("/bigWebApp/hello", response.getRedirect());
ep = new AuthenticationProcessingFilterEntryPoint();
ep.setLoginFormUrl("/hello");
ep.setForceHttps(true);
HashMap map = new HashMap();
try {
ep.setHttpsPortMapping(map);
} catch (IllegalArgumentException expected) {
assertEquals("must map at least one port", expected.getMessage());
}
map.put(new Integer(0).toString(), new Integer(443).toString());
try {
ep.setHttpsPortMapping(map);
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().startsWith("one or both ports out of legal range"));
}
map.clear();
map.put(new Integer(80).toString(), new Integer(100000).toString());
try {
ep.setHttpsPortMapping(map);
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().startsWith("one or both ports out of legal range"));
}
map.clear();
map.put(new Integer(80).toString(), new Integer(443).toString());
ep.setHttpsPortMapping(map);
map = ep.getTranslatedHttpsPortMapping();
assertTrue(map.size() == 1);
assertTrue(((Integer)map.get(new Integer(80))).equals(new Integer(443)));
map.put("8888", "9999");
ep.setHttpsPortMappings(map);
ep.afterPropertiesSet();
ep.commence(request, response);
assertEquals("https://www.example.com:9999/bigWebApp/hello",
response.getRedirect());
}
public void testNormalOperation() throws Exception {
@ -104,11 +115,39 @@ public class AuthenticationProcessingFilterEntryPointTests extends TestCase {
ep.commence(request, response);
assertEquals("/bigWebApp/hello", response.getRedirect());
}
public void testHttpsOperation() throws Exception {
public void testSetSslPortMapping() {
AuthenticationProcessingFilterEntryPoint ep = new AuthenticationProcessingFilterEntryPoint();
//TODO: finish later today
HashMap map = new HashMap();
try {
ep.setHttpsPortMappings(map);
} catch (IllegalArgumentException expected) {
assertEquals("must map at least one port", expected.getMessage());
}
map.put(new Integer(0).toString(), new Integer(443).toString());
try {
ep.setHttpsPortMappings(map);
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().startsWith("one or both ports out of legal range"));
}
map.clear();
map.put(new Integer(80).toString(), new Integer(100000).toString());
try {
ep.setHttpsPortMappings(map);
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().startsWith("one or both ports out of legal range"));
}
map.clear();
map.put(new Integer(80).toString(), new Integer(443).toString());
ep.setHttpsPortMappings(map);
map = ep.getTranslatedHttpsPortMappings();
assertTrue(map.size() == 1);
assertTrue(((Integer) map.get(new Integer(80))).equals(new Integer(443)));
}
}

View File

@ -1,5 +1,5 @@
#HSQL database
#Fri Apr 16 07:51:49 EDT 2004
#Thu Apr 22 17:27:10 EDT 2004
sql.strict_fk=true
readonly=false
sql.strong_fk=true