From 31234ecef9c0c012a5086cd7e1b3295386069f6c Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Wed, 4 Feb 2015 05:21:41 +0900 Subject: [PATCH] SEC-2835: Add DelegatingAuthenticationFailureHandler Add the DelegatingAuthenticationFailureHandler class to support map each exception to AuthenticationFailureHandler. This class gives more powerful options to customize default behavior for users. --- ...elegatingAuthenticationFailureHandler.java | 85 +++++++++++ ...tingAuthenticationFailureHandlerTests.java | 143 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandler.java create mode 100644 web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandlerTests.java diff --git a/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandler.java b/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandler.java new file mode 100644 index 0000000000..0186fa2589 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.web.authentication; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.util.Assert; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An {@link AuthenticationFailureHandler} that delegates to other + * {@link AuthenticationFailureHandler} instances based upon the type of + * {@link AuthenticationException} passed into + * {@link #onAuthenticationFailure(HttpServletRequest, HttpServletResponse, AuthenticationException)}. + * + * @author Kazuki Shimizu + * @since 4.0 + */ +public class DelegatingAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final LinkedHashMap, AuthenticationFailureHandler> handlers; + + private final AuthenticationFailureHandler defaultHandler; + + /** + * Creates a new instance + * + * @param handlers + * a map of the {@link AuthenticationException} class to the + * {@link AuthenticationFailureHandler} that should be used. + * Each is considered in the order they are specified and only + * the first {@link AuthenticationFailureHandler} is ued. + * This parameter cannot specify null or empty. + * @param defaultHandler + * the default {@link AuthenticationFailureHandler} + * that should be used if none of the handlers matches. + * This parameter cannot specify null. + * @throws IllegalArgumentException if invalid argument is specified + */ + public DelegatingAuthenticationFailureHandler( + LinkedHashMap, AuthenticationFailureHandler> handlers, + AuthenticationFailureHandler defaultHandler) { + Assert.notEmpty(handlers, "handlers cannot be null or empty"); + Assert.notNull(defaultHandler, "defaultHandler cannot be null"); + this.handlers = handlers; + this.defaultHandler = defaultHandler; + } + + /** + * {@inheritDoc} + */ + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, AuthenticationException exception) + throws IOException, ServletException { + for (Map.Entry, AuthenticationFailureHandler> entry : handlers.entrySet()) { + Class handlerMappedExceptionClass = entry.getKey(); + if (handlerMappedExceptionClass.isAssignableFrom(exception.getClass())) { + AuthenticationFailureHandler handler = entry.getValue(); + handler.onAuthenticationFailure(request, response, exception); + return; + } + } + defaultHandler.onAuthenticationFailure(request, response, exception); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandlerTests.java new file mode 100644 index 0000000000..0d2785703f --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationFailureHandlerTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.web.authentication; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.authentication.AccountExpiredException; +import org.springframework.security.authentication.AccountStatusException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.core.AuthenticationException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.LinkedHashMap; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test class for {@link org.springframework.security.web.authentication.DelegatingAuthenticationFailureHandler} + * + * @author Kazuki shimizu + * @since 4.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingAuthenticationFailureHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private AuthenticationFailureHandler handler1; + + @Mock + private AuthenticationFailureHandler handler2; + + @Mock + private AuthenticationFailureHandler defaultHandler; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private LinkedHashMap, AuthenticationFailureHandler> handlers; + + private DelegatingAuthenticationFailureHandler handler; + + @Before + public void setup() { + handlers = new LinkedHashMap, AuthenticationFailureHandler>(); + } + + @Test + public void handleByDefaultHandler() throws Exception { + handlers.put(BadCredentialsException.class, handler1); + handler = new DelegatingAuthenticationFailureHandler(handlers, defaultHandler); + + AuthenticationException exception = new AccountExpiredException(""); + handler.onAuthenticationFailure(request, response, exception); + + verifyZeroInteractions(handler1, handler2); + verify(defaultHandler).onAuthenticationFailure(request, response, exception); + } + + @Test + public void handleByMappedHandlerWithSameType() throws Exception { + handlers.put(BadCredentialsException.class, handler1); // same type + handlers.put(AccountStatusException.class, handler2); + handler = new DelegatingAuthenticationFailureHandler(handlers, defaultHandler); + + AuthenticationException exception = new BadCredentialsException(""); + handler.onAuthenticationFailure(request, response, exception); + + verifyZeroInteractions(handler2, defaultHandler); + verify(handler1).onAuthenticationFailure(request, response, exception); + } + + @Test + public void handleByMappedHandlerWithSuperType() throws Exception { + handlers.put(BadCredentialsException.class, handler1); + handlers.put(AccountStatusException.class, handler2); // super type of CredentialsExpiredException + handler = new DelegatingAuthenticationFailureHandler(handlers, defaultHandler); + + AuthenticationException exception = new CredentialsExpiredException(""); + handler.onAuthenticationFailure(request, response, exception); + + verifyZeroInteractions(handler1, defaultHandler); + verify(handler2).onAuthenticationFailure(request, response, exception); + } + + @Test + public void handlersIsNull() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("handlers cannot be null or empty"); + + new DelegatingAuthenticationFailureHandler(null, defaultHandler); + + } + + @Test + public void handlersIsEmpty() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("handlers cannot be null or empty"); + + new DelegatingAuthenticationFailureHandler(handlers, defaultHandler); + + } + + @Test + public void defaultHandlerIsNull() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("defaultHandler cannot be null"); + + handlers.put(BadCredentialsException.class, handler1); + new DelegatingAuthenticationFailureHandler(handlers, null); + + } + +}