diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/RequestMatcherLogoutSuccessHandler.java b/web/src/main/java/org/springframework/security/web/authentication/logout/RequestMatcherLogoutSuccessHandler.java new file mode 100644 index 0000000000..186c4a92d3 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/logout/RequestMatcherLogoutSuccessHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2016 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.logout; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.JavascriptOriginRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +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; + +/** + * Delegates to logout handlers based on matched request matchers + * + * @author Shazin Sadakath + */ +public class RequestMatcherLogoutSuccessHandler implements LogoutSuccessHandler { + + private Map requestMatcherLogoutSuccessHandlers = new LinkedHashMap(); + + public RequestMatcherLogoutSuccessHandler() { + requestMatcherLogoutSuccessHandlers.put(new JavascriptOriginRequestMatcher(), new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)); + requestMatcherLogoutSuccessHandlers.put(new NegatedRequestMatcher(new JavascriptOriginRequestMatcher()), new SimpleUrlLogoutSuccessHandler()); + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + for(Map.Entry entry : requestMatcherLogoutSuccessHandlers.entrySet()) { + if(entry.getKey().matches(request)) { + entry.getValue().onLogoutSuccess(request, response, authentication); + break; + } + } + } + + public Map getRequestMatcherLogoutSuccessHandlers() { + return requestMatcherLogoutSuccessHandlers; + } + + public void setRequestMatcherLogoutSuccessHandlers(Map requestMatcherLogoutSuccessHandlers) { + Assert.notNull(requestMatcherLogoutSuccessHandlers, "must not be null"); + this.requestMatcherLogoutSuccessHandlers = requestMatcherLogoutSuccessHandlers; + } +} diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/JavascriptOriginRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/JavascriptOriginRequestMatcher.java new file mode 100644 index 0000000000..9799de90c4 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/matcher/JavascriptOriginRequestMatcher.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2016 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.util.matcher; + +import javax.servlet.http.HttpServletRequest; + +/** + * Decides whether the Request Originated from Javascript. + * + * @author Shazin Sadakath + */ +public class JavascriptOriginRequestMatcher implements RequestMatcher { + + public static final String HTTP_X_REQUESTED_WITH = "HTTP_X_REQUESTED_WITH"; + public static final String XML_HTTP_REQUEST = "XMLHttpRequest"; + + private String headerName = HTTP_X_REQUESTED_WITH; + private String headerValue = XML_HTTP_REQUEST; + + + @Override + public boolean matches(HttpServletRequest request) { + Object xHttpRequestedWith = request.getHeader(headerName); + return xHttpRequestedWith != null && xHttpRequestedWith.toString().equalsIgnoreCase(headerValue); + } + + public String getHeaderName() { + return headerName; + } + + public void setHeaderName(String headerName) { + this.headerName = headerName; + } + + public String getHeaderValue() { + return headerValue; + } + + public void setHeaderValue(String headerValue) { + this.headerValue = headerValue; + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/logout/RequestMatcherLogoutSuccessHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/logout/RequestMatcherLogoutSuccessHandlerTests.java new file mode 100644 index 0000000000..a028ec2da4 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/logout/RequestMatcherLogoutSuccessHandlerTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2016 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.logout; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.IpAddressMatcher; +import org.springframework.security.web.util.matcher.JavascriptOriginRequestMatcher; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * RequestMatcherLogoutSuccessHandler Tests + * + * @author Shazin Sadakath + */ +@RunWith(MockitoJUnitRunner.class) +public class RequestMatcherLogoutSuccessHandlerTests { + + private RequestMatcherLogoutSuccessHandler customLogoutSuccessHandler = new RequestMatcherLogoutSuccessHandler(); + + @Before + public void init() { + Map requestMatcherLogoutSuccessHandlerMap = new LinkedHashMap(); + requestMatcherLogoutSuccessHandlerMap.put(new IpAddressMatcher("192.168.1.5"), new SimpleUrlLogoutSuccessHandler()); + requestMatcherLogoutSuccessHandlerMap.put(new MediaTypeRequestMatcher(new HeaderContentNegotiationStrategy(), MediaType.APPLICATION_JSON), new HttpStatusReturningLogoutSuccessHandler(HttpStatus.CREATED)); + customLogoutSuccessHandler.setRequestMatcherLogoutSuccessHandlers(requestMatcherLogoutSuccessHandlerMap); + } + + @Test + public void javascriptOriginRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + LogoutSuccessHandler logoutSuccessHandler = new RequestMatcherLogoutSuccessHandler(); + + request.addHeader(JavascriptOriginRequestMatcher.HTTP_X_REQUESTED_WITH, "XMLHttpRequest"); + + logoutSuccessHandler.onLogoutSuccess(request, response, mock(Authentication.class)); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Test + public void nonJavascriptOriginRequest() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + LogoutSuccessHandler logoutSuccessHandler = new RequestMatcherLogoutSuccessHandler(); + + logoutSuccessHandler.onLogoutSuccess(request, response, mock(Authentication.class)); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); + } + + @Test + public void customRequestMatcherHandlerMap_IPAddress() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.1.5"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + customLogoutSuccessHandler.onLogoutSuccess(request, response, mock(Authentication.class)); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); + } + + @Test + public void customRequestMatcherHandlerMap_AcceptHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + MockHttpServletResponse response = new MockHttpServletResponse(); + + customLogoutSuccessHandler.onLogoutSuccess(request, response, mock(Authentication.class)); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value()); + } + + @Test + public void customRequestMatcherHandlerMap_IPAddressAndAcceptHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.1.5"); + request.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + MockHttpServletResponse response = new MockHttpServletResponse(); + + customLogoutSuccessHandler.onLogoutSuccess(request, response, mock(Authentication.class)); + + // IPAddressRequestMatcher -> SimpleUrlLogoutSuccessHandler will be invoked first + assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value()); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/JavascriptOriginRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/JavascriptOriginRequestMatcherTests.java new file mode 100644 index 0000000000..b5ae11d9c1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/matcher/JavascriptOriginRequestMatcherTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2016 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.util.matcher; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * JavascriptOriginRequestMatcher Tests + * + * @author Shazin Sadakath + */ +@RunWith(MockitoJUnitRunner.class) +public class JavascriptOriginRequestMatcherTests { + + private RequestMatcher matcher = new JavascriptOriginRequestMatcher(); + + @Test + public void javascriptOriginRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(JavascriptOriginRequestMatcher.HTTP_X_REQUESTED_WITH, "XMLHttpRequest"); + + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void nonJavascriptOriginRequest_EmptyHeader() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(JavascriptOriginRequestMatcher.HTTP_X_REQUESTED_WITH, ""); + + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void nonJavascriptOriginRequest_NotSetHeader() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + assertThat(matcher.matches(request)).isFalse(); + } + +}