diff --git a/cas/src/main/java/org/springframework/security/ui/cas/CasProcessingFilter.java b/cas/src/main/java/org/springframework/security/ui/cas/CasProcessingFilter.java index 1860f18e76..7438e04591 100644 --- a/cas/src/main/java/org/springframework/security/ui/cas/CasProcessingFilter.java +++ b/cas/src/main/java/org/springframework/security/ui/cas/CasProcessingFilter.java @@ -78,7 +78,7 @@ public class CasProcessingFilter extends AbstractProcessingFilter { private ProxyGrantingTicketStorage proxyGrantingTicketStorage; //~ Methods ======================================================================================================== - public Authentication attemptAuthentication(final HttpServletRequest request) + public Authentication attemptAuthentication(final HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { final String username = CAS_STATEFUL_IDENTIFIER; String password = request.getParameter("ticket"); diff --git a/cas/src/test/java/org/springframework/security/ui/cas/CasProcessingFilterTests.java b/cas/src/test/java/org/springframework/security/ui/cas/CasProcessingFilterTests.java index 49729f07e0..e377bd6b8e 100644 --- a/cas/src/test/java/org/springframework/security/ui/cas/CasProcessingFilterTests.java +++ b/cas/src/test/java/org/springframework/security/ui/cas/CasProcessingFilterTests.java @@ -22,6 +22,7 @@ import org.springframework.security.AuthenticationException; import org.springframework.security.MockAuthenticationManager; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; /** @@ -31,26 +32,8 @@ import org.springframework.mock.web.MockHttpServletRequest; * @version $Id$ */ public class CasProcessingFilterTests extends TestCase { - //~ Constructors =================================================================================================== - - public CasProcessingFilterTests() { - super(); - } - - public CasProcessingFilterTests(String arg0) { - super(arg0); - } - //~ Methods ======================================================================================================== - public static void main(String[] args) { - junit.textui.TestRunner.run(CasProcessingFilterTests.class); - } - - public final void setUp() throws Exception { - super.setUp(); - } - public void testGetters() { CasProcessingFilter filter = new CasProcessingFilter(); assertEquals("/j_spring_cas_security_check", filter.getDefaultFilterProcessesUrl()); @@ -66,7 +49,7 @@ public class CasProcessingFilterTests extends TestCase { filter.setAuthenticationManager(authMgr); filter.init(null); - Authentication result = filter.attemptAuthentication(request); + Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); assertTrue(result != null); } @@ -81,7 +64,7 @@ public class CasProcessingFilterTests extends TestCase { filter.init(null); try { - filter.attemptAuthentication(request); + filter.attemptAuthentication(request, new MockHttpServletResponse()); fail("Should have thrown AuthenticationException"); } catch (AuthenticationException expected) { assertTrue(true); diff --git a/core/src/main/java/org/springframework/security/concurrent/ConcurrentSessionFilter.java b/core/src/main/java/org/springframework/security/concurrent/ConcurrentSessionFilter.java index e4ef0f8570..5b626ab3ee 100644 --- a/core/src/main/java/org/springframework/security/concurrent/ConcurrentSessionFilter.java +++ b/core/src/main/java/org/springframework/security/concurrent/ConcurrentSessionFilter.java @@ -24,6 +24,7 @@ import org.springframework.security.ui.logout.SecurityContextLogoutHandler; import org.springframework.security.util.UrlUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -61,7 +62,8 @@ public class ConcurrentSessionFilter extends SpringSecurityFilter implements Ini public void afterPropertiesSet() throws Exception { Assert.notNull(sessionRegistry, "SessionRegistry required"); - Assert.isTrue(UrlUtils.isValidRedirectUrl(expiredUrl), expiredUrl + " isn't a valid redirect URL"); + Assert.isTrue(expiredUrl == null || UrlUtils.isValidRedirectUrl(expiredUrl), + expiredUrl + " isn't a valid redirect URL"); } public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) @@ -120,7 +122,7 @@ public class ConcurrentSessionFilter extends SpringSecurityFilter implements Ini } public void setLogoutHandlers(LogoutHandler[] handlers) { - Assert.notNull(handlers); + Assert.notNull(handlers); this.handlers = handlers; } diff --git a/core/src/main/java/org/springframework/security/config/ConfigUtils.java b/core/src/main/java/org/springframework/security/config/ConfigUtils.java index f59346e89b..b6d6033e5d 100644 --- a/core/src/main/java/org/springframework/security/config/ConfigUtils.java +++ b/core/src/main/java/org/springframework/security/config/ConfigUtils.java @@ -157,7 +157,7 @@ abstract class ConfigUtils { * If not empty or starting with "$" (potential placeholder), "/" or "http" it will raise an error. */ static void validateHttpRedirect(String url, ParserContext pc, Object source) { - if (UrlUtils.isValidRedirectUrl(url) || url.startsWith("$")) { + if (!StringUtils.hasText(url) || UrlUtils.isValidRedirectUrl(url) || url.startsWith("$")) { return; } pc.getReaderContext().warning(url + " is not a valid redirect URL (must start with '/' or http(s))", source); diff --git a/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java index 622640c0be..fe40306d2c 100644 --- a/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java +++ b/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.ui.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.ui.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint; import org.springframework.security.ui.webapp.DefaultLoginPageGeneratingFilter; import org.springframework.util.StringUtils; @@ -37,6 +38,7 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser { private static final String DEF_FORM_LOGIN_AUTHENTICATION_FAILURE_URL = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL + "?" + DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME; private static final String ATT_SUCCESS_HANDLER_REF = "authentication-success-handler-ref"; + private static final String ATT_FAILURE_HANDLER_REF = "authentication-failure-handler-ref"; private String defaultLoginProcessingUrl; private String filterClassName; @@ -56,6 +58,7 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser { String authenticationFailureUrl = null; String alwaysUseDefault = null; String successHandlerRef = null; + String failureHandlerRef = null; Object source = null; @@ -82,18 +85,18 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser { alwaysUseDefault = elt.getAttribute(ATT_ALWAYS_USE_DEFAULT_TARGET_URL); loginPage = elt.getAttribute(ATT_LOGIN_PAGE); successHandlerRef = elt.getAttribute(ATT_SUCCESS_HANDLER_REF); + failureHandlerRef = elt.getAttribute(ATT_FAILURE_HANDLER_REF); if (!StringUtils.hasText(loginPage)) { loginPage = null; } ConfigUtils.validateHttpRedirect(loginPage, pc, source); - } ConfigUtils.registerProviderManagerIfNecessary(pc); filterBean = createFilterBean(loginUrl, defaultTargetUrl, alwaysUseDefault, loginPage, authenticationFailureUrl, - successHandlerRef); + successHandlerRef, failureHandlerRef); filterBean.setSource(source); filterBean.getPropertyValues().addPropertyValue("authenticationManager", new RuntimeBeanReference(BeanIds.AUTHENTICATION_MANAGER)); @@ -123,7 +126,7 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser { } private RootBeanDefinition createFilterBean(String loginUrl, String defaultTargetUrl, String alwaysUseDefault, - String loginPage, String authenticationFailureUrl, String successHandlerRef) { + String loginPage, String authenticationFailureUrl, String successHandlerRef, String failureHandlerRef) { BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.rootBeanDefinition(filterClassName); @@ -144,17 +147,22 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser { filterBuilder.addPropertyValue("successHandler", successHandler.getBeanDefinition()); } - if (!StringUtils.hasText(authenticationFailureUrl)) { - // Fallback to redisplaying the custom login page, if one was specified - if (StringUtils.hasText(loginPage)) { - authenticationFailureUrl = loginPage; - } else { - authenticationFailureUrl = DEF_FORM_LOGIN_AUTHENTICATION_FAILURE_URL; + if (StringUtils.hasText(failureHandlerRef)) { + filterBuilder.addPropertyReference("failureHandler", failureHandlerRef); + } else { + BeanDefinitionBuilder failureHandler = BeanDefinitionBuilder.rootBeanDefinition(SimpleUrlAuthenticationFailureHandler.class); + if (!StringUtils.hasText(authenticationFailureUrl)) { + // Fall back to redisplaying the custom login page, if one was specified. + if (StringUtils.hasText(loginPage)) { + authenticationFailureUrl = loginPage; + } else { + authenticationFailureUrl = DEF_FORM_LOGIN_AUTHENTICATION_FAILURE_URL; + } } + failureHandler.addPropertyValue("defaultFailureUrl", authenticationFailureUrl); + filterBuilder.addPropertyValue("failureHandler", failureHandler.getBeanDefinition()); } - filterBuilder.addPropertyValue("authenticationFailureUrl", authenticationFailureUrl); - return (RootBeanDefinition) filterBuilder.getBeanDefinition(); } diff --git a/core/src/main/java/org/springframework/security/ui/AbstractProcessingFilter.java b/core/src/main/java/org/springframework/security/ui/AbstractProcessingFilter.java index 5c487839c5..7f1949cb86 100644 --- a/core/src/main/java/org/springframework/security/ui/AbstractProcessingFilter.java +++ b/core/src/main/java/org/springframework/security/ui/AbstractProcessingFilter.java @@ -19,7 +19,6 @@ import org.springframework.security.SpringSecurityMessageSource; import org.springframework.security.Authentication; import org.springframework.security.AuthenticationException; import org.springframework.security.AuthenticationManager; -import org.springframework.security.util.RedirectUtils; import org.springframework.security.util.SessionUtils; import org.springframework.security.util.UrlUtils; @@ -44,8 +43,6 @@ import org.springframework.util.Assert; import java.io.IOException; -import java.util.Properties; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -128,45 +125,24 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl //~ Instance fields ================================================================================================ protected ApplicationEventPublisher eventPublisher; - protected AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); - private AuthenticationManager authenticationManager; - protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); - private Properties exceptionMappings = new Properties(); - - /** + /* * Delay use of NullRememberMeServices until initialization so that namespace has a chance to inject * the RememberMeServices implementation into custom implementations. */ private RememberMeServices rememberMeServices = null; - /** Where to redirect the browser to if authentication fails */ - private String authenticationFailureUrl; - /** * The URL destination that this filter intercepts and processes (usually * something like /j_spring_security_check) */ private String filterProcessesUrl = getDefaultFilterProcessesUrl(); - /** - * Indicates if the filter chain should be continued prior to delegation to - * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, - * Authentication)}, which may be useful in certain environment (eg - * Tapestry). Defaults to false. - */ private boolean continueChainBeforeSuccessfulAuthentication = false; - /** - * If true, causes any redirection URLs to be calculated minus the protocol - * and context path (defaults to false). - */ - protected boolean useRelativeContext = false; - - /** * Tells if we on successful authentication should invalidate the * current session. This is a common guard against session fixation attacks. @@ -185,19 +161,16 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl private boolean allowSessionCreation = true; - private boolean serverSideRedirect = false; - private SessionRegistry sessionRegistry; private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); - private AuthenticationFailureHandler failureHandler = null; + private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); //~ Methods ======================================================================================================== public void afterPropertiesSet() throws Exception { Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified"); Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL"); - Assert.isTrue(UrlUtils.isValidRedirectUrl(authenticationFailureUrl), authenticationFailureUrl + " isn't a valid redirect URL"); Assert.notNull(authenticationManager, "authenticationManager must be specified"); if (rememberMeServices == null) { @@ -206,19 +179,26 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl } /** - * Performs actual authentication. - * - * @param request from which to extract parameters and perform the - * authentication - * - * @return the authenticated user - * - * @throws AuthenticationException if authentication fails + * Invokes the {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse) requiresAuthentication} + * method to determine whether the request is for authentication and should be handled by this filter. + * If it is an authentication request, the + * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse) attemptAuthentication} will be invoked + * to perform the authentication. There are then three possible outcomes: + *
    + *
  1. An Authentication object is returned. + * The {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, Authentication) + * successfulAuthentication} method will be invoked
  2. + *
  3. An AuthenticationException occurs during authentication. + * The {@link #unSuccessfulAuthentication(HttpServletRequest, HttpServletResponse, Authentication) + * unSuccessfulAuthentication} method will be invoked
  4. + *
  5. Null is returned, indicating that the authentication process is incomplete. + * The method will then return immediately, assuming that the subclass has done any necessary work (such as + * redirects) to continue the authentication process. The assumption is that a later request will be received + * by this method where the returned Authentication object is not null. + *
*/ - public abstract Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException; - - public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, - ServletException { + public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); @@ -233,8 +213,11 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl Authentication authResult; try { - onPreAuthentication(request, response); - authResult = attemptAuthentication(request); + authResult = attemptAuthentication(request, response); + if (authResult == null) { + // return immediately as subclass has indicated that it hasn't completed authentication + return; + } } catch (AuthenticationException failed) { // Authentication failed @@ -251,35 +234,17 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl successfulAuthentication(request, response, authResult); } - protected void onPreAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException, IOException { - } - - protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException failed) throws IOException { - } - /** - *

- * Indicates whether this filter should attempt to process a login request - * for the current invocation. - *

+ * Indicates whether this filter should attempt to process a login request for the current invocation. *

* It strips any parameters from the "path" section of the request URL (such * as the jsessionid parameter in * http://host/myapp/index.html;jsessionid=blah) before matching * against the filterProcessesUrl property. - *

*

- * Subclasses may override for special requirements, such as Tapestry - * integration. - *

+ * Subclasses may override for special requirements, such as Tapestry integration. * - * @param request as received from the filter chain - * @param response as received from the filter chain - * - * @return true if the filter should attempt authentication, - * false otherwise + * @return true if the filter should attempt authentication, false otherwise. */ protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { String uri = request.getRequestURI(); @@ -297,6 +262,41 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl return uri.endsWith(request.getContextPath() + filterProcessesUrl); } + /** + * Performs actual authentication. + *

+ * The implementation should do one of the following: + *

    + *
  1. Return a populated authentication token for the authenticated user, indicating successful authentication
  2. + *
  3. Return null, indicating that the authentication process is still in progress. Before returning, the + * implementation should perform any additional work required to complete the process.
  4. + *
  5. Throw an AuthenticationException if the authentication process fails
  6. + *
+ * + * @param request from which to extract parameters and perform the authentication + * @param response the response, which may be needed if the implementation has to do a redirect as part of a + * multi-stage authentication process (such as OpenID). + * + * @return the authenticated user token, or null if authentication is incomplete. + * + * @throws AuthenticationException if authentication fails. + */ + public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException; + + /** + * Default behaviour for successful authentication. + *
    + *
  1. Sets the successful Authentication object on the {@link SecurityContextHolder}
  2. + *
  3. Performs any configured session migration behaviour
  4. + *
  5. Informs the configured RememberMeServices of the successul login
  6. + *
  7. Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured + * ApplicationEventPublisher
  8. + *
  9. Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.
  10. + *
+ * + * @param authResult the object returned from the attemptAuthentication method. + */ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException, ServletException { @@ -310,28 +310,24 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl SessionUtils.startNewSessionIfRequired(request, migrateInvalidatedSessionAttributes, sessionRegistry); } - successHandler.onAuthenticationSuccess(request, response, authResult); - rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } + + successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(null); - if (logger.isDebugEnabled()) { - logger.debug("Updated SecurityContextHolder to contain null Authentication"); - } - - String failureUrl = determineFailureUrl(request, failed); - if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString()); + logger.debug("Updated SecurityContextHolder to contain null Authentication"); + logger.debug("Delegating to authentication failure handler" + failureHandler); } try { @@ -344,29 +340,9 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl catch (Exception ignored) { } - onUnsuccessfulAuthentication(request, response, failed); - rememberMeServices.loginFail(request, response); - if (failureUrl == null) { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed:" + failed.getMessage()); - } else if (serverSideRedirect){ - request.getRequestDispatcher(failureUrl).forward(request, response); - } else { - RedirectUtils.sendRedirect(request, response, failureUrl, useRelativeContext); - } - } - - protected String determineFailureUrl(HttpServletRequest request, AuthenticationException failed) { - return exceptionMappings.getProperty(failed.getClass().getName(), authenticationFailureUrl); - } - - public String getAuthenticationFailureUrl() { - return authenticationFailureUrl; - } - - public void setAuthenticationFailureUrl(String authenticationFailureUrl) { - this.authenticationFailureUrl = authenticationFailureUrl; + failureHandler.onAuthenticationFailure(request, response, failed); } protected AuthenticationManager getAuthenticationManager() { @@ -378,21 +354,12 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl } /** - * Specifies the default filterProcessesUrl for the - * implementation. + * Specifies the default filterProcessesUrl for the implementation. * * @return the default filterProcessesUrl */ public abstract String getDefaultFilterProcessesUrl(); - protected Properties getExceptionMappings() { - return new Properties(exceptionMappings); - } - - public void setExceptionMappings(Properties exceptionMappings) { - this.exceptionMappings = exceptionMappings; - } - public String getFilterProcessesUrl() { return filterProcessesUrl; } @@ -409,6 +376,12 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl this.rememberMeServices = rememberMeServices; } + /** + * Indicates if the filter chain should be continued prior to delegation to + * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, + * Authentication)}, which may be useful in certain environment (such as + * Tapestry applications). Defaults to false. + */ public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) { this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication; } @@ -439,10 +412,6 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl return authenticationDetailsSource; } - public void setUseRelativeContext(boolean useRelativeContext) { - this.useRelativeContext = useRelativeContext; - } - protected boolean getAllowSessionCreation() { return allowSessionCreation; } @@ -451,15 +420,6 @@ public abstract class AbstractProcessingFilter extends SpringSecurityFilter impl this.allowSessionCreation = allowSessionCreation; } - /** - * Tells if we are to do a server side include of the error URL instead of a 302 redirect. - * - * @param serverSideRedirect - */ - public void setServerSideRedirect(boolean serverSideRedirect) { - this.serverSideRedirect = serverSideRedirect; - } - /** * The session registry needs to be set if session fixation attack protection is in use (and concurrent * session control is enabled). diff --git a/core/src/main/java/org/springframework/security/ui/ExceptionMappingAuthenticationFailureHandler.java b/core/src/main/java/org/springframework/security/ui/ExceptionMappingAuthenticationFailureHandler.java index 205c570c02..5282206e80 100644 --- a/core/src/main/java/org/springframework/security/ui/ExceptionMappingAuthenticationFailureHandler.java +++ b/core/src/main/java/org/springframework/security/ui/ExceptionMappingAuthenticationFailureHandler.java @@ -4,11 +4,14 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.AuthenticationException; import org.springframework.security.util.RedirectUtils; +import org.springframework.security.util.UrlUtils; +import org.springframework.util.Assert; /** * Uses the internal map of exceptions types to URLs to determine the destination on authentication failure. The keys @@ -26,7 +29,7 @@ public class ExceptionMappingAuthenticationFailureHandler extends SimpleUrlAuthe @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException { + AuthenticationException exception) throws IOException, ServletException { String url = failureUrlMap.get(exception.getClass().getName()); if (url != null) { @@ -36,5 +39,23 @@ public class ExceptionMappingAuthenticationFailureHandler extends SimpleUrlAuthe } } - + /** + * Sets the map of exception types (by name) to URLs. + * + * @param failureUrlMap the map keyed by the fully-qualified name of the exception class, with the corresponding + * failure URL as the value. + * + * @throws IllegalArgumentException if the entries are not Strings or the URL is not valid. + */ + public void setExceptionMappings(Map failureUrlMap) { + this.failureUrlMap.clear(); + for (Map.Entry entry : failureUrlMap.entrySet()) { + Object exception = entry.getKey(); + Object url = entry.getValue(); + Assert.isInstanceOf(String.class, exception, "Exception key must be a String (the exception classname)."); + Assert.isInstanceOf(String.class, url, "URL must be a String"); + Assert.isTrue(UrlUtils.isValidRedirectUrl((String)url), "Not a valid redirect URL: " + url); + this.failureUrlMap.put((String)exception, (String)url); + } + } } diff --git a/core/src/main/java/org/springframework/security/ui/SavedRequestAwareAuthenticationSuccessHandler.java b/core/src/main/java/org/springframework/security/ui/SavedRequestAwareAuthenticationSuccessHandler.java index 61e03b9dbd..384361b3ef 100644 --- a/core/src/main/java/org/springframework/security/ui/SavedRequestAwareAuthenticationSuccessHandler.java +++ b/core/src/main/java/org/springframework/security/ui/SavedRequestAwareAuthenticationSuccessHandler.java @@ -14,6 +14,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.Authentication; import org.springframework.security.ui.savedrequest.SavedRequest; import org.springframework.security.util.RedirectUtils; +import org.springframework.security.util.UrlUtils; import org.springframework.security.wrapper.SavedRequestAwareWrapper; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -163,7 +164,7 @@ public class SavedRequestAwareAuthenticationSuccessHandler implements Authentica * @param defaultTargetUrl */ public void setDefaultTargetUrl(String defaultTargetUrl) { - Assert.isTrue(defaultTargetUrl.startsWith("/") | defaultTargetUrl.startsWith("http"), + Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultTargetUrl), "defaultTarget must start with '/' or with 'http(s)'"); this.defaultTargetUrl = defaultTargetUrl; } diff --git a/core/src/main/java/org/springframework/security/ui/SimpleUrlAuthenticationFailureHandler.java b/core/src/main/java/org/springframework/security/ui/SimpleUrlAuthenticationFailureHandler.java index 31ed2e3885..b6a50889ea 100644 --- a/core/src/main/java/org/springframework/security/ui/SimpleUrlAuthenticationFailureHandler.java +++ b/core/src/main/java/org/springframework/security/ui/SimpleUrlAuthenticationFailureHandler.java @@ -2,11 +2,14 @@ package org.springframework.security.ui; import java.io.IOException; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.AuthenticationException; import org.springframework.security.util.RedirectUtils; +import org.springframework.security.util.UrlUtils; +import org.springframework.util.Assert; /** * AuthenticationFailureHandler which performs a redirect to the value of the {@link #setDefaultFailureUrl @@ -31,11 +34,15 @@ public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFail private boolean useRelativeContext = false; public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException { + AuthenticationException exception) throws IOException, ServletException { if (defaultFailureUrl == null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed:" + exception.getMessage()); } else { - RedirectUtils.sendRedirect(request, response, defaultFailureUrl, useRelativeContext); + if (forwardToDestination) { + request.getRequestDispatcher(defaultFailureUrl).forward(request, response); + } else { + RedirectUtils.sendRedirect(request, response, defaultFailureUrl, useRelativeContext); + } } } @@ -44,7 +51,8 @@ public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFail * * @param defaultFailureUrl the failure URL, for example "/loginFailed.jsp". */ - public void setDefaultTargetUrl(String defaultFailureUrl) { + public void setDefaultFailureUrl(String defaultFailureUrl) { + Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl)); this.defaultFailureUrl = defaultFailureUrl; } diff --git a/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilter.java b/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilter.java index eaf15f9c55..25ba563f22 100644 --- a/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilter.java +++ b/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilter.java @@ -26,6 +26,7 @@ import org.springframework.security.util.TextUtils; import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -53,7 +54,7 @@ public class AuthenticationProcessingFilter extends AbstractProcessingFilter { //~ Methods ======================================================================================================== - public Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = obtainUsername(request); String password = obtainPassword(request); diff --git a/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPoint.java b/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPoint.java index fa34febf56..15333d6cc3 100644 --- a/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPoint.java +++ b/core/src/main/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPoint.java @@ -24,6 +24,7 @@ import org.springframework.security.util.PortMapperImpl; import org.springframework.security.util.PortResolver; import org.springframework.security.util.PortResolverImpl; import org.springframework.security.util.RedirectUrlBuilder; +import org.springframework.security.util.UrlUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -31,6 +32,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import java.io.IOException; @@ -79,7 +81,8 @@ public class AuthenticationProcessingFilterEntryPoint implements AuthenticationE //~ Methods ======================================================================================================== public void afterPropertiesSet() throws Exception { - Assert.hasLength(loginFormUrl, "loginFormUrl must be specified"); + Assert.isTrue(StringUtils.hasText(loginFormUrl) && UrlUtils.isValidRedirectUrl(loginFormUrl), + "loginFormUrl must be specified and must be a valid redirect URL"); Assert.notNull(portMapper, "portMapper must be specified"); Assert.notNull(portResolver, "portResolver must be specified"); } @@ -117,7 +120,7 @@ public class AuthenticationProcessingFilterEntryPoint implements AuthenticationE if (redirectUrl == null) { String loginForm = determineUrlToUseForThisRequest(httpRequest, httpResponse, authException); - + if (logger.isDebugEnabled()) { logger.debug("Server side forward to: " + loginForm); } @@ -246,7 +249,7 @@ public class AuthenticationProcessingFilterEntryPoint implements AuthenticationE */ public void setServerSideRedirect(boolean serverSideRedirect) { this.serverSideRedirect = serverSideRedirect; - } + } protected boolean isServerSideRedirect() { return serverSideRedirect; diff --git a/core/src/main/java/org/springframework/security/util/UrlUtils.java b/core/src/main/java/org/springframework/security/util/UrlUtils.java index b4be3056f9..7ad6ea0524 100644 --- a/core/src/main/java/org/springframework/security/util/UrlUtils.java +++ b/core/src/main/java/org/springframework/security/util/UrlUtils.java @@ -15,13 +15,11 @@ package org.springframework.security.util; -import org.springframework.security.intercept.web.FilterInvocation; - -import org.springframework.security.ui.savedrequest.SavedRequest; -import org.springframework.util.StringUtils; - import javax.servlet.http.HttpServletRequest; +import org.springframework.security.intercept.web.FilterInvocation; +import org.springframework.security.ui.savedrequest.SavedRequest; + /** * Provides static methods for composing URLs.

Placed into a separate class for visibility, so that changes to @@ -102,8 +100,11 @@ public final class UrlUtils { return buildRequestUrl(sr.getServletPath(), sr.getRequestURI(), sr.getContextPath(), sr.getPathInfo(), sr.getQueryString()); } - + + /** + * Returns true if the supplied URL starts with a "/" or "http". + */ public static boolean isValidRedirectUrl(String url) { - return !StringUtils.hasText(url) || url.startsWith("/") || url.toLowerCase().startsWith("http"); + return url.startsWith("/") || url.toLowerCase().startsWith("http"); } } diff --git a/core/src/test/java/org/springframework/security/ui/AbstractProcessingFilterTests.java b/core/src/test/java/org/springframework/security/ui/AbstractProcessingFilterTests.java index 49f68bcfa1..d03e89b76b 100644 --- a/core/src/test/java/org/springframework/security/ui/AbstractProcessingFilterTests.java +++ b/core/src/test/java/org/springframework/security/ui/AbstractProcessingFilterTests.java @@ -55,6 +55,7 @@ import org.springframework.security.util.PortResolverImpl; */ public class AbstractProcessingFilterTests extends TestCase { SavedRequestAwareAuthenticationSuccessHandler successHandler; + SimpleUrlAuthenticationFailureHandler failureHandler; //~ Methods ======================================================================================================== private MockHttpServletRequest createMockRequest() { @@ -102,6 +103,8 @@ public class AbstractProcessingFilterTests extends TestCase { super.setUp(); successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setDefaultTargetUrl("/logged_in.jsp"); + failureHandler = new SimpleUrlAuthenticationFailureHandler(); + failureHandler.setDefaultFailureUrl("/failed.jsp"); SecurityContextHolder.clearContext(); } @@ -133,7 +136,7 @@ public class AbstractProcessingFilterTests extends TestCase { // Setup our test object, to deny access MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(false); - filter.setAuthenticationFailureUrl("/failed.jsp"); + filter.setFailureHandler(failureHandler); // Test executeFilterInContainerSimulator(config, filter, request, response, chain); @@ -143,11 +146,11 @@ public class AbstractProcessingFilterTests extends TestCase { //Prepare again, this time using the exception mapping filter = new MockAbstractProcessingFilter(new AccountExpiredException("You're account is expired")); - filter.setAuthenticationFailureUrl("/failed.jsp"); - - Properties exceptionMappings = filter.getExceptionMappings(); + ExceptionMappingAuthenticationFailureHandler failureHandler = new ExceptionMappingAuthenticationFailureHandler(); + filter.setFailureHandler(failureHandler); + Properties exceptionMappings = new Properties(); exceptionMappings.setProperty(AccountExpiredException.class.getName(), "/accountExpired.jsp"); - filter.setExceptionMappings(exceptionMappings); + failureHandler.setExceptionMappings(exceptionMappings); response = new MockHttpServletResponse(); // Test @@ -186,7 +189,6 @@ public class AbstractProcessingFilterTests extends TestCase { AbstractProcessingFilter filter = new MockAbstractProcessingFilter(); filter.setAuthenticationManager(new MockAuthenticationManager()); filter.setFilterProcessesUrl("/p"); - filter.setAuthenticationFailureUrl("/fail"); filter.afterPropertiesSet(); assertNotNull(filter.getRememberMeServices()); @@ -194,7 +196,6 @@ public class AbstractProcessingFilterTests extends TestCase { assertEquals(TokenBasedRememberMeServices.class, filter.getRememberMeServices().getClass()); assertTrue(filter.getAuthenticationManager() != null); assertEquals("/p", filter.getFilterProcessesUrl()); - assertEquals("/fail", filter.getAuthenticationFailureUrl()); } public void testIgnoresAnyServletPathOtherThanFilterProcessesUrl() throws Exception { @@ -234,7 +235,7 @@ public class AbstractProcessingFilterTests extends TestCase { filter.setFilterProcessesUrl("/j_mock_post"); filter.setSuccessHandler(successHandler); - filter.setAuthenticationFailureUrl("/failure.jsp"); + filter.setFailureHandler(failureHandler); filter.setAuthenticationManager(new MockAuthenticationManager(true)); filter.afterPropertiesSet(); @@ -249,7 +250,7 @@ public class AbstractProcessingFilterTests extends TestCase { public void testStartupDetectsInvalidAuthenticationManager() throws Exception { AbstractProcessingFilter filter = new MockAbstractProcessingFilter(); - filter.setAuthenticationFailureUrl("/failed.jsp"); + filter.setFailureHandler(failureHandler); successHandler.setDefaultTargetUrl("/"); filter.setSuccessHandler(successHandler); filter.setFilterProcessesUrl("/j_spring_security_check"); @@ -264,7 +265,7 @@ public class AbstractProcessingFilterTests extends TestCase { public void testStartupDetectsInvalidFilterProcessesUrl() throws Exception { AbstractProcessingFilter filter = new MockAbstractProcessingFilter(); - filter.setAuthenticationFailureUrl("/failed.jsp"); + filter.setFailureHandler(failureHandler); filter.setAuthenticationManager(new MockAuthenticationManager()); filter.setSuccessHandler(successHandler); filter.setFilterProcessesUrl(null); @@ -308,7 +309,7 @@ public class AbstractProcessingFilterTests extends TestCase { // Setup our test object, to deny access filter = new MockAbstractProcessingFilter(false); filter.setFilterProcessesUrl("/j_mock_post"); - filter.setAuthenticationFailureUrl("/failed.jsp"); + filter.setFailureHandler(failureHandler); // Test executeFilterInContainerSimulator(config, filter, request, response, chain); @@ -468,7 +469,7 @@ public class AbstractProcessingFilterTests extends TestCase { // Reject authentication, so exception would normally be stored in session MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(false); filter.setAllowSessionCreation(false); - filter.setAuthenticationFailureUrl("/"); + filter.setFailureHandler(failureHandler); successHandler.setDefaultTargetUrl("http://monkeymachine.co.uk/"); filter.setSuccessHandler(successHandler); @@ -509,8 +510,9 @@ public class AbstractProcessingFilterTests extends TestCase { MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(false); successHandler.setDefaultTargetUrl("http://monkeymachine.co.uk/"); filter.setSuccessHandler(successHandler); - filter.setAuthenticationFailureUrl("/error"); - filter.setServerSideRedirect(true); + filter.setFailureHandler(failureHandler); + failureHandler.setForwardToDestination(true); + failureHandler.setDefaultFailureUrl("/error"); executeFilterInContainerSimulator(config, filter, request, response, chain); @@ -532,7 +534,7 @@ public class AbstractProcessingFilterTests extends TestCase { filter.setSuccessHandler(successHandler); successHandler.setDefaultTargetUrl("http://monkeymachine.co.uk/"); successHandler.setTargetUrlParameter("targetUrl"); - filter.setAuthenticationFailureUrl("/error"); + filter.setFailureHandler(failureHandler); executeFilterInContainerSimulator(config, filter, request, response, chain); @@ -561,7 +563,7 @@ public class AbstractProcessingFilterTests extends TestCase { private MockAbstractProcessingFilter() { } - public Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (grantAccess) { return new UsernamePasswordAuthenticationToken("test", "test", AuthorityUtils.createAuthorityList("TEST")); } else { diff --git a/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java b/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java index 3d95ca4eb7..008feaafad 100644 --- a/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java +++ b/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java @@ -47,7 +47,6 @@ public class AuthenticationProcessingFilterEntryPointTests extends TestCase { ep.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { - assertEquals("loginFormUrl must be specified", expected.getMessage()); } } @@ -60,7 +59,6 @@ public class AuthenticationProcessingFilterEntryPointTests extends TestCase { ep.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { - assertEquals("portMapper must be specified", expected.getMessage()); } } @@ -73,7 +71,6 @@ public class AuthenticationProcessingFilterEntryPointTests extends TestCase { ep.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { - assertEquals("portResolver must be specified", expected.getMessage()); } } @@ -126,7 +123,7 @@ public class AuthenticationProcessingFilterEntryPointTests extends TestCase { assertEquals("https://www.example.com:8443/bigWebApp/hello", response.getRedirectedUrl()); PortMapperImpl portMapper = new PortMapperImpl(); - Map map = new HashMap(); + Map map = new HashMap(); map.put("8888", "9999"); portMapper.setPortMappings(map); response = new MockHttpServletResponse(); diff --git a/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterTests.java b/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterTests.java index 5c2c105fc5..be026856d3 100644 --- a/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterTests.java +++ b/core/src/test/java/org/springframework/security/ui/webapp/AuthenticationProcessingFilterTests.java @@ -24,8 +24,10 @@ import org.springframework.security.AuthenticationException; import org.springframework.security.ui.WebAuthenticationDetails; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; /** @@ -60,7 +62,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { filter.setAuthenticationManager(new MockAuthenticationManager(true)); filter.init(null); - Authentication result = filter.attemptAuthentication(request); + Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); assertTrue(result != null); assertEquals("rod", request.getSession().getAttribute( AuthenticationProcessingFilter.SPRING_SECURITY_LAST_USERNAME_KEY)); @@ -74,7 +76,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { AuthenticationProcessingFilter filter = new AuthenticationProcessingFilter(); filter.setAuthenticationManager(new MockAuthenticationManager(true)); - Authentication result = filter.attemptAuthentication(request); + Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); assertTrue(result != null); } @@ -85,7 +87,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { AuthenticationProcessingFilter filter = new AuthenticationProcessingFilter(); filter.setAuthenticationManager(new MockAuthenticationManager(true)); - Authentication result = filter.attemptAuthentication(request); + Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); assertTrue(result != null); } @@ -99,7 +101,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { request.addParameter("x", "rod"); request.addParameter("y", "koala"); - Authentication result = filter.attemptAuthentication(request); + Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); assertTrue(result != null); assertEquals("127.0.0.1", ((WebAuthenticationDetails) result.getDetails()).getRemoteAddress()); } @@ -112,7 +114,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { AuthenticationProcessingFilter filter = new AuthenticationProcessingFilter(); filter.setAuthenticationManager(new MockAuthenticationManager(true)); - Authentication result = filter.attemptAuthentication(request); + Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); assertEquals("rod", result.getName()); } @@ -123,7 +125,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { filter.setAuthenticationManager(new MockAuthenticationManager(false)); try { - filter.attemptAuthentication(request); + filter.attemptAuthentication(request, new MockHttpServletResponse()); fail("Expected AuthenticationException"); } catch (AuthenticationException e) { } @@ -143,7 +145,7 @@ public class AuthenticationProcessingFilterTests extends TestCase { filter.setAllowSessionCreation(false); filter.setAuthenticationManager(new MockAuthenticationManager(true)); - filter.attemptAuthentication(request); + filter.attemptAuthentication(request, new MockHttpServletResponse()); assertNull(request.getSession(false)); } diff --git a/core/src/test/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilterTests.java b/core/src/test/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilterTests.java index 43ac0ff8d9..e5ba381de3 100644 --- a/core/src/test/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilterTests.java +++ b/core/src/test/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilterTests.java @@ -3,6 +3,7 @@ package org.springframework.security.ui.webapp; import static org.junit.Assert.*; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -38,7 +39,7 @@ public class DefaultLoginPageGeneratingFilterTests { private static class MockProcessingFilter extends AbstractProcessingFilter { @Override - public Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { return null; } diff --git a/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml b/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml index 0467f22b5e..71c3e6b1a8 100644 --- a/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml +++ b/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml @@ -32,7 +32,6 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc - diff --git a/openid/src/main/java/org/springframework/security/providers/openid/OpenIDAuthenticationToken.java b/openid/src/main/java/org/springframework/security/providers/openid/OpenIDAuthenticationToken.java index 907f795296..b9242960a8 100644 --- a/openid/src/main/java/org/springframework/security/providers/openid/OpenIDAuthenticationToken.java +++ b/openid/src/main/java/org/springframework/security/providers/openid/OpenIDAuthenticationToken.java @@ -15,18 +15,16 @@ package org.springframework.security.providers.openid; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.springframework.security.GrantedAuthority; - import org.springframework.security.providers.AbstractAuthenticationToken; - /** * OpenID Authentication Token * - * @author Robin Bramley, Opsera Ltd + * @author Robin Bramley + * @version $Id$ */ public class OpenIDAuthenticationToken extends AbstractAuthenticationToken { //~ Instance fields ================================================================================================ @@ -60,7 +58,8 @@ public class OpenIDAuthenticationToken extends AbstractAuthenticationToken { //~ Methods ======================================================================================================== - /* (non-Javadoc) + /** + * Returns 'null' always, as no credentials are processed by the OpenID provider. * @see org.springframework.security.Authentication#getCredentials() */ public Object getCredentials() { @@ -75,7 +74,8 @@ public class OpenIDAuthenticationToken extends AbstractAuthenticationToken { return message; } - /* (non-Javadoc) + /** + * Returns the identityUrl value. * @see org.springframework.security.Authentication#getPrincipal() */ public Object getPrincipal() { diff --git a/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilter.java b/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilter.java index 6783ddbd9b..1ba10e9b22 100644 --- a/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilter.java +++ b/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilter.java @@ -15,47 +15,67 @@ package org.springframework.security.ui.openid; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + import org.springframework.security.Authentication; import org.springframework.security.AuthenticationException; import org.springframework.security.AuthenticationServiceException; -import org.springframework.security.context.SecurityContextHolder; +import org.springframework.security.providers.openid.OpenIDAuthenticationProvider; import org.springframework.security.providers.openid.OpenIDAuthenticationToken; import org.springframework.security.ui.AbstractProcessingFilter; import org.springframework.security.ui.FilterChainOrder; import org.springframework.security.ui.openid.consumers.OpenID4JavaConsumer; import org.springframework.security.ui.webapp.AuthenticationProcessingFilter; -import org.springframework.security.util.RedirectUtils; import org.springframework.util.StringUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - /** - * @author Robin Bramley, Opsera Ltd + * Filter which processes OpenID authentication requests. + *

+ * The OpenID authentication involves two stages. + * + *

Submission of OpenID identity

+ * + * The user's OpenID identity is submitted via a login form, just as it would be for a normal form login. At this stage + * the filter will extract the identity from the submitted request (by default, the parameter is called + * j_username, as it is for form login. It then passes the identity to the configured OpenIDConsumer, + * which returns the URL to which the request should be redirected for authentication. A "return_to" URL is also supplied, + * which matches the URL processed by this filter, to allow the filter to handle the request once the user has + * been successfully authenticated. The OpenID server will then authenticate the user and redirect back to the + * application. + * + *

Processing the Redirect from the OpenID Server

+ * + * Once the user has been authenticated externally, the redirected request will be passed to the OpenIDConsumer + * again for validation. The returned OpenIDAuthentication will be passed to the AuthenticationManager + * where it should (normally) be processed by an OpenIDAuthenticationProvider in order to load the authorities + * for the user. + * + * @author Robin Bramley * @author Ray Krueger + * @author Luke Taylor * @version $Id$ * @since 2.0 + * @see OpenIDAuthenticationProvider */ public class OpenIDAuthenticationProcessingFilter extends AbstractProcessingFilter { //~ Static fields/initializers ===================================================================================== - private static final Log log = LogFactory.getLog(OpenIDAuthenticationProcessingFilter.class); public static final String DEFAULT_CLAIMED_IDENTITY_FIELD = "j_username"; //~ Instance fields ================================================================================================ private OpenIDConsumer consumer; private String claimedIdentityFieldName = DEFAULT_CLAIMED_IDENTITY_FIELD; - private Map realmMapping = new HashMap(); + private Map realmMapping = Collections.emptyMap(); //~ Methods ======================================================================================================== @@ -66,31 +86,65 @@ public class OpenIDAuthenticationProcessingFilter extends AbstractProcessingFilt } } - public Authentication attemptAuthentication(HttpServletRequest req) throws AuthenticationException { + public String getDefaultFilterProcessesUrl() { + return "/j_spring_openid_security_check"; + } + + /** + * Authentication has two phases. + *
    + *
  1. The initial submission of the claimed OpenID. A redirect to the URL returned from the consumer + * will be performed and null will be returned.
  2. + *
  3. The redirection from the OpenID server to the return_to URL, once it has authenticated the user
  4. + *
+ */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException { OpenIDAuthenticationToken token; - String identity = req.getParameter("openid.identity"); + String identity = request.getParameter("openid.identity"); if (!StringUtils.hasText(identity)) { + String claimedIdentity = obtainUsername(request); // Make the username available to the view - String username = obtainUsername(req); - setLastUsername(username, req); - throw new OpenIDAuthenticationRequiredException("External Authentication Required", username); + setLastUsername(claimedIdentity, request); + + try { + String returnToUrl = buildReturnToUrl(request); + String realm = lookupRealm(returnToUrl); + String openIdUrl = consumer.beginConsumption(request, claimedIdentity, returnToUrl, realm); + if (logger.isDebugEnabled()) { + logger.debug("return_to is '" + returnToUrl + "', realm is '" + realm + "'"); + logger.debug("Redirecting to " + openIdUrl); + } + response.sendRedirect(openIdUrl); + + // Indicate to parent class that authentication is continuing. + return null; + } catch (OpenIDConsumerException e) { + logger.debug("Failed to consume claimedIdentity: " + claimedIdentity, e); + throw new AuthenticationServiceException("Unable to process claimed identity '" + claimedIdentity + "'"); + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Supplied OpenID identity is " + identity); } try { - token = consumer.endConsumption(req); + token = consumer.endConsumption(request); } catch (OpenIDConsumerException oice) { throw new AuthenticationServiceException("Consumer error", oice); } - token.setDetails(authenticationDetailsSource.buildDetails(req)); + token.setDetails(authenticationDetailsSource.buildDetails(request)); - // delegate to the auth provider + // delegate to the authentication provider Authentication authentication = this.getAuthenticationManager().authenticate(token); if (authentication.isAuthenticated()) { - setLastUsername(token.getIdentityUrl(), req); + setLastUsername(token.getIdentityUrl(), request); } return authentication; @@ -104,28 +158,8 @@ public class OpenIDAuthenticationProcessingFilter extends AbstractProcessingFilt } } - protected String determineFailureUrl(HttpServletRequest request, AuthenticationException failed) { - if (failed instanceof OpenIDAuthenticationRequiredException) { - OpenIDAuthenticationRequiredException openIdRequiredException = (OpenIDAuthenticationRequiredException) failed; - String claimedIdentity = openIdRequiredException.getClaimedIdentity(); - - if (StringUtils.hasText(claimedIdentity)) { - try { - String returnToUrl = buildReturnToUrl(request); - String realm = lookupRealm(returnToUrl); - return consumer.beginConsumption(request, claimedIdentity, returnToUrl, realm); - } catch (OpenIDConsumerException e) { - log.error("Unable to consume claimedIdentity [" + claimedIdentity + "]", e); - } - } - } - - return super.determineFailureUrl(request, failed); - } - protected String lookupRealm(String returnToUrl) { - - String mapping = (String) realmMapping.get(returnToUrl); + String mapping = realmMapping.get(returnToUrl); if (mapping == null) { try { @@ -140,61 +174,52 @@ public class OpenIDAuthenticationProcessingFilter extends AbstractProcessingFilt .append("/"); mapping = realmBuffer.toString(); } catch (MalformedURLException e) { - log.warn("returnToUrl was not a valid URL: [" + returnToUrl + "]", e); + logger.warn("returnToUrl was not a valid URL: [" + returnToUrl + "]", e); } } return mapping; } + /** + * Builds the return_to URL that will be sent to the OpenID service provider. + * By default returns the URL of the current request. + * + * @param request the current request which is being processed by this filter + * @return The return_to URL. + */ protected String buildReturnToUrl(HttpServletRequest request) { return request.getRequestURL().toString(); } - public String getClaimedIdentityFieldName() { - return claimedIdentityFieldName; - } - - public OpenIDConsumer getConsumer() { - return consumer; - } - - public String getDefaultFilterProcessesUrl() { - return "/j_spring_openid_security_check"; - } - - protected boolean isAuthenticated(HttpServletRequest request) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - return (auth != null) && auth.isAuthenticated(); - } - /** - * The OpenIdAuthenticationProcessingFilter will ignore the request coming in if this method returns false. - * The default functionality checks if the request scheme starts with http.
This method should be overridden in subclasses that wish to consider a different strategy - * - * @param request HttpServletRequest we're processing - * @return true if this request is determined to be an OpenID request. + * Reads the claimedIdentityFieldName from the submitted request. */ - protected boolean isOpenIdRequest(HttpServletRequest request) { - String username = obtainUsername(request); - return (StringUtils.hasText(username)) && username.toLowerCase().startsWith("http"); - } - protected String obtainUsername(HttpServletRequest req) { return req.getParameter(claimedIdentityFieldName); } - protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException failed) throws IOException { - if (failed instanceof OpenIDAuthenticationRequiredException) { - OpenIDAuthenticationRequiredException openIdAuthenticationRequiredException = (OpenIDAuthenticationRequiredException) failed; - request.setAttribute(OpenIDAuthenticationRequiredException.class.getName(), - openIdAuthenticationRequiredException.getClaimedIdentity()); - } + /** + * Maps the return_to url to a realm, for example: + *
+     * http://www.example.com/j_spring_openid_security_check -> http://www.example.com/realm
+     * 
+ * If no mapping is provided then the returnToUrl will be parsed to extract the protocol, hostname and port followed + * by a trailing slash. + * This means that http://www.example.com/j_spring_openid_security_check will automatically become + * http://www.example.com:80/ + * + * @param realmMapping containing returnToUrl -> realm mappings + */ + public void setRealmMapping(Map realmMapping) { + this.realmMapping = realmMapping; } + /** + * The name of the request parameter containing the OpenID identity, as submitted from the initial login form. + * + * @param claimedIdentityFieldName defaults to "j_username" + */ public void setClaimedIdentityFieldName(String claimedIdentityFieldName) { this.claimedIdentityFieldName = claimedIdentityFieldName; } @@ -203,61 +228,7 @@ public class OpenIDAuthenticationProcessingFilter extends AbstractProcessingFilt this.consumer = consumer; } - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException failed) throws IOException { - SecurityContextHolder.getContext().setAuthentication(null); - - if (logger.isDebugEnabled()) { - logger.debug("Updated SecurityContextHolder to contain null Authentication"); - } - - String failureUrl = determineFailureUrl(request, failed); - - if (logger.isDebugEnabled()) { - logger.debug("Authentication request failed: " + failed.toString()); - } - - if (getAllowSessionCreation()) { - try { - request.getSession().setAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY, failed); - } catch (Exception ignored) { - } - } - - super.getRememberMeServices().loginFail(request, response); - - RedirectUtils.sendRedirect(request, response, failureUrl, useRelativeContext); - } - public int getOrder() { return FilterChainOrder.OPENID_PROCESSING_FILTER; } - - /** - * Maps the return_to url to a realm.
- * For example http://www.example.com/j_spring_openid_security_check -> http://www.example.com/realm
- * If no mapping is provided then the returnToUrl will be parsed to extract the protocol, hostname and port followed - * by a trailing slash.
- * This means that http://www.example.com/j_spring_openid_security_check will automatically - * become http://www.example.com:80/ - * - * @return Map containing returnToUrl -> realm mappings - */ - public Map getRealmMapping() { - return realmMapping; - } - - /** - * Maps the return_to url to a realm.
- * For example http://www.example.com/j_spring_openid_security_check -> http://www.example.com/realm
- * If no mapping is provided then the returnToUrl will be parsed to extract the protocol, hostname and port followed - * by a trailing slash.
- * This means that http://www.example.com/j_spring_openid_security_check will automatically - * become http://www.example.com:80/ - * - * @param realmMapping containing returnToUrl -> realm mappings - */ - public void setRealmMapping(Map realmMapping) { - this.realmMapping = realmMapping; - } } diff --git a/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationRequiredException.java b/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationRequiredException.java deleted file mode 100644 index d5115832ff..0000000000 --- a/openid/src/main/java/org/springframework/security/ui/openid/OpenIDAuthenticationRequiredException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* Copyright 2004, 2005, 2006 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 org.springframework.security.ui.openid; - -import org.springframework.security.AuthenticationException; - -/** - * @author Ray Krueger - */ -public class OpenIDAuthenticationRequiredException extends AuthenticationException { - - private final String claimedIdentity; - - public OpenIDAuthenticationRequiredException(String msg, String claimedIdentity) { - super(msg); - this.claimedIdentity = claimedIdentity; - } - - public String getClaimedIdentity() { - return claimedIdentity; - } -} diff --git a/openid/src/test/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilterTests.java b/openid/src/test/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilterTests.java index f5ab4495fa..b40796d989 100644 --- a/openid/src/test/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilterTests.java +++ b/openid/src/test/java/org/springframework/security/ui/openid/OpenIDAuthenticationProcessingFilterTests.java @@ -29,16 +29,6 @@ public class OpenIDAuthenticationProcessingFilterTests extends TestCase { filter.afterPropertiesSet(); } - public void testNoIdentityCausesException() throws Exception { - try { - MockHttpServletRequest req = new MockHttpServletRequest(); - filter.attemptAuthentication(req); - fail("OpenIDAuthenticationRequiredException expected, no openid.identity parameter"); - } catch (OpenIDAuthenticationRequiredException e) { - //cool - } - } - public void testFilterOperation() throws Exception { MockHttpServletRequest req = new MockHttpServletRequest("GET", REQUEST_PATH); MockHttpServletResponse response = new MockHttpServletResponse(); diff --git a/samples/openid/src/main/webapp/WEB-INF/classes/log4j.properties b/samples/openid/src/main/webapp/WEB-INF/classes/log4j.properties index 2f7890f02e..ddd6feb06c 100644 --- a/samples/openid/src/main/webapp/WEB-INF/classes/log4j.properties +++ b/samples/openid/src/main/webapp/WEB-INF/classes/log4j.properties @@ -1,20 +1,12 @@ # Global logging configuration -log4j.rootLogger=INFO, stdout, fileout +log4j.rootLogger=INFO, stdout -log4j.logger.org.springframework.security=DEBUG, stdout, fileout +log4j.logger.org.springframework.security=DEBUG # Console output... log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.conversionPattern=[%p,%c{1},%t] %m%n - -# Rolling log file output... -log4j.appender.fileout=org.apache.log4j.RollingFileAppender -log4j.appender.fileout.File=spring-security-openid.log -#log4j.appender.fileout.File=${webapp.root}/WEB-INF/log4j.log -log4j.appender.fileout.MaxFileSize=1024KB -log4j.appender.fileout.MaxBackupIndex=1 -log4j.appender.fileout.layout=org.apache.log4j.PatternLayout -log4j.appender.fileout.layout.conversionPattern=%d{ABSOLUTE} %5p %c{1},%t:%L - %m%n +log4j.appender.stdout.layout.conversionPattern=[%p,%c{1}] %m%n + diff --git a/samples/openid/src/main/webapp/WEB-INF/web.xml b/samples/openid/src/main/webapp/WEB-INF/web.xml index b203f61cfc..9cc9e28f00 100644 --- a/samples/openid/src/main/webapp/WEB-INF/web.xml +++ b/samples/openid/src/main/webapp/WEB-INF/web.xml @@ -12,20 +12,25 @@ Spring Security Preauthentication Demo Application - - contextConfigLocation - - /WEB-INF/applicationContext-security.xml - - + - Location of the XML file that defines the root application context + - Applied by ContextLoaderListener. + --> + + contextConfigLocation + + /WEB-INF/applicationContext-security.xml + + log4jConfigLocation /WEB-INF/classes/log4j.properties - + + + + webAppRootKey + openid.root + springSecurityFilterChain @@ -37,22 +42,22 @@ /* - - - org.springframework.web.context.ContextLoaderListener - + + org.springframework.web.context.ContextLoaderListener + org.springframework.web.util.Log4jConfigListener - + org.springframework.security.ui.session.HttpSessionEventPublisher diff --git a/samples/tutorial/src/main/webapp/WEB-INF/web.xml b/samples/tutorial/src/main/webapp/WEB-INF/web.xml index 4aaf71d2af..8b0f75af45 100644 --- a/samples/tutorial/src/main/webapp/WEB-INF/web.xml +++ b/samples/tutorial/src/main/webapp/WEB-INF/web.xml @@ -10,23 +10,28 @@ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> Spring Security Tutorial Application - + - - contextConfigLocation - - classpath:applicationContext-business.xml - /WEB-INF/applicationContext-security.xml - - - + - Location of the XML file that defines the root application context + - Applied by ContextLoaderListener. + --> + + contextConfigLocation + + classpath:applicationContext-business.xml + /WEB-INF/applicationContext-security.xml + + + log4jConfigLocation /WEB-INF/classes/log4j.properties - + + + + webAppRootKey + tutorial.root + springSecurityFilterChain @@ -38,43 +43,43 @@ /* - - - org.springframework.web.context.ContextLoaderListener - + + org.springframework.web.context.ContextLoaderListener + - org.springframework.security.ui.session.HttpSessionEventPublisher - + org.springframework.web.util.Log4jConfigListener - + - - bank - org.springframework.web.servlet.DispatcherServlet - 1 - + + bank + org.springframework.web.servlet.DispatcherServlet + 1 + - - bank - *.html - + + bank + *.html + - index.jsp - + index.jsp +