1
0
mirror of synced 2026-08-05 01:36:56 +00:00

SEC-2422: Session timeout detection with CSRF protection

This commit is contained in:
Rob Winch
2013-12-11 17:38:17 -06:00
parent 00d668dc5c
commit 7f714ebb23
13 changed files with 523 additions and 30 deletions
@@ -0,0 +1,81 @@
/*
* Copyright 2002-2013 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.access;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
/**
* An {@link AccessDeniedHandler} that delegates to other
* {@link AccessDeniedHandler} instances based upon the type of
* {@link AccessDeniedException} passed into
* {@link #handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)}.
*
* @author Rob Winch
* @since 3.2
*
*/
public final class DelegatingAccessDeniedHandler implements AccessDeniedHandler {
private final LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers;
private final AccessDeniedHandler defaultHander;
/**
* Creates a new instance
*
* @param handlers
* a map of the {@link AccessDeniedException} class to the
* {@link AccessDeniedHandler} that should be used. Each is
* considered in the order they are specified and only the first
* {@link AccessDeniedHandler} is ued.
* @param defaultHander
* the default {@link AccessDeniedHandler} that should be used if
* none of the handlers matches.
*/
public DelegatingAccessDeniedHandler(
LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers,
AccessDeniedHandler defaultHander) {
Assert.notEmpty(handlers, "handlers cannot be null or empty");
Assert.notNull(defaultHander, "defaultHandler cannot be null");
this.handlers = handlers;
this.defaultHander = defaultHander;
}
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
for(Entry<Class<? extends AccessDeniedException>, AccessDeniedHandler> entry : handlers.entrySet()) {
Class<? extends AccessDeniedException> handlerClass = entry.getKey();
if(handlerClass.isAssignableFrom(accessDeniedException.getClass())) {
AccessDeniedHandler handler = entry.getValue();
handler.handle(request, response, accessDeniedException);
return;
}
}
defaultHander.handle(request, response, accessDeniedException);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2013 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.csrf;
import org.springframework.security.access.AccessDeniedException;
/**
* Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest
*
* @author Rob Winch
* @since 3.2
*/
@SuppressWarnings("serial")
public class CsrfException extends AccessDeniedException {
public CsrfException(String message) {
super(message);
}
}
@@ -73,7 +73,8 @@ public final class CsrfFilter extends OncePerRequestFilter {
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = tokenRepository.loadToken(request);
if(csrfToken == null) {
final boolean missingToken = csrfToken == null;
if(missingToken) {
CsrfToken generatedToken = tokenRepository.generateToken(request);
csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response, generatedToken);
}
@@ -93,7 +94,11 @@ public final class CsrfFilter extends OncePerRequestFilter {
if(logger.isDebugEnabled()) {
logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
if(missingToken) {
accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
@@ -15,17 +15,17 @@
*/
package org.springframework.security.web.csrf;
import org.springframework.security.access.AccessDeniedException;
import javax.servlet.http.HttpServletRequest;
/**
* Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest
* Thrown when an expected {@link CsrfToken} exists, but it does not match the
* value present on the {@link HttpServletRequest}
*
* @author Rob Winch
* @since 3.2
*/
@SuppressWarnings("serial")
public class InvalidCsrfTokenException extends AccessDeniedException {
public class InvalidCsrfTokenException extends CsrfException {
/**
* @param msg
@@ -36,5 +36,4 @@ public class InvalidCsrfTokenException extends AccessDeniedException {
+ expectedAccessToken.getParameterName() + "' or header '"
+ expectedAccessToken.getHeaderName() + "'.");
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2002-2013 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.csrf;
/**
* Thrown when no expected {@link CsrfToken} is found but is required.
*
* @author Rob Winch
* @since 3.2
*/
@SuppressWarnings("serial")
public class MissingCsrfTokenException extends CsrfException {
public MissingCsrfTokenException(String actualToken) {
super("Expected CSRF token not found. Has your session expired?");
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2002-2013 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.session;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.util.Assert;
/**
* An adapter of {@link InvalidSessionStrategy} to {@link AccessDeniedHandler}
*
* @author Rob Winch
* @since 3.2
*/
public final class InvalidSessionAccessDeniedHandler implements AccessDeniedHandler {
private final InvalidSessionStrategy invalidSessionStrategy;
/**
* Creates a new instance
* @param invalidSessionStrategy the {@link InvalidSessionStrategy} to delegate to
*/
public InvalidSessionAccessDeniedHandler(
InvalidSessionStrategy invalidSessionStrategy) {
Assert.notNull(invalidSessionStrategy, "invalidSessionStrategy cannot be null");
this.invalidSessionStrategy = invalidSessionStrategy;
}
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
invalidSessionStrategy.onInvalidSessionDetected(request, response);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2002-2013 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.access;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import java.util.LinkedHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
@RunWith(MockitoJUnitRunner.class)
public class DelegatingAccessDeniedHandlerTests {
@Mock
private AccessDeniedHandler handler1;
@Mock
private AccessDeniedHandler handler2;
@Mock
private AccessDeniedHandler handler3;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
private LinkedHashMap<Class<? extends AccessDeniedException>,AccessDeniedHandler> handlers;
private DelegatingAccessDeniedHandler handler;
@Before
public void setup() {
handlers = new LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler>();
}
@Test
public void moreSpecificDoesNotInvokeLessSpecific() throws Exception {
handlers.put(CsrfException.class, handler1);
handler = new DelegatingAccessDeniedHandler(handlers, handler3);
AccessDeniedException accessDeniedException = new AccessDeniedException("");
handler.handle(request, response, accessDeniedException);
verify(handler1,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class));
verify(handler3).handle(request, response, accessDeniedException);
}
@Test
public void matchesDoesNotInvokeDefault() throws Exception {
handlers.put(InvalidCsrfTokenException.class, handler1);
handlers.put(MissingCsrfTokenException.class, handler2);
handler = new DelegatingAccessDeniedHandler(handlers, handler3);
AccessDeniedException accessDeniedException = new MissingCsrfTokenException("123");
handler.handle(request, response, accessDeniedException);
verify(handler1,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class));
verify(handler2).handle(request, response, accessDeniedException);
verify(handler3,never()).handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class));
}
}