Reactive Resource Server Csrf Bypass

This makes requests identified as bearer token requests skip the csrf
filter.

Fixes: gh-5710
This commit is contained in:
Josh Cummings 2018-08-23 12:56:46 -06:00 committed by Rob Winch
parent 820fb7d828
commit 68d836d508
2 changed files with 77 additions and 2 deletions

View File

@ -43,6 +43,7 @@ import org.springframework.security.authorization.AuthenticatedReactiveAuthoriza
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
@ -114,7 +115,9 @@ import org.springframework.security.web.server.savedrequest.ServerRequestCacheWe
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter; import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter;
import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter; import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
@ -130,6 +133,8 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry; import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch;
/** /**
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux. * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux.
@ -703,6 +708,9 @@ public class ServerHttpSecurity {
public class JwtSpec { public class JwtSpec {
private ReactiveJwtDecoder jwtDecoder; private ReactiveJwtDecoder jwtDecoder;
private BearerTokenServerWebExchangeMatcher bearerTokenServerWebExchangeMatcher =
new BearerTokenServerWebExchangeMatcher();
/** /**
* Configures the {@link ReactiveJwtDecoder} to use * Configures the {@link ReactiveJwtDecoder} to use
* @param jwtDecoder the decoder to use * @param jwtDecoder the decoder to use
@ -740,13 +748,20 @@ public class ServerHttpSecurity {
} }
protected void configure(ServerHttpSecurity http) { protected void configure(ServerHttpSecurity http) {
ServerBearerTokenAuthenticationConverter bearerTokenConverter =
new ServerBearerTokenAuthenticationConverter();
this.bearerTokenServerWebExchangeMatcher.setBearerTokenConverter(bearerTokenConverter);
registerDefaultCsrfOverride(http);
BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
ReactiveJwtDecoder jwtDecoder = getJwtDecoder(); ReactiveJwtDecoder jwtDecoder = getJwtDecoder();
JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager( JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager(
jwtDecoder); jwtDecoder);
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter()); oauth2.setServerAuthenticationConverter(bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint)); oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
http http
.exceptionHandling() .exceptionHandling()
.authenticationEntryPoint(entryPoint) .authenticationEntryPoint(entryPoint)
@ -760,6 +775,38 @@ public class ServerHttpSecurity {
} }
return this.jwtDecoder; return this.jwtDecoder;
} }
private void registerDefaultCsrfOverride(ServerHttpSecurity http) {
if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) {
http
.csrf()
.requireCsrfProtectionMatcher(
new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(
this.bearerTokenServerWebExchangeMatcher)));
}
}
private class BearerTokenServerWebExchangeMatcher implements ServerWebExchangeMatcher {
ServerBearerTokenAuthenticationConverter bearerTokenConverter;
@Override
public Mono<MatchResult> matches(ServerWebExchange exchange) {
return this.bearerTokenConverter.convert(exchange)
.flatMap(this::nullAuthentication)
.onErrorResume(e -> notMatch());
}
public void setBearerTokenConverter(ServerBearerTokenAuthenticationConverter bearerTokenConverter) {
Assert.notNull(bearerTokenConverter, "bearerTokenConverter cannot be null");
this.bearerTokenConverter = bearerTokenConverter;
}
private Mono<MatchResult> nullAuthentication(Authentication authentication) {
return authentication == null ? notMatch() : match();
}
}
} }
public ServerHttpSecurity and() { public ServerHttpSecurity and() {
@ -1173,6 +1220,8 @@ public class ServerHttpSecurity {
public class CsrfSpec { public class CsrfSpec {
private CsrfWebFilter filter = new CsrfWebFilter(); private CsrfWebFilter filter = new CsrfWebFilter();
private boolean specifiedRequireCsrfProtectionMatcher;
/** /**
* Configures the {@link ServerAccessDeniedHandler} used when a CSRF token is invalid. Default is * Configures the {@link ServerAccessDeniedHandler} used when a CSRF token is invalid. Default is
* to send an {@link org.springframework.http.HttpStatus#FORBIDDEN}. * to send an {@link org.springframework.http.HttpStatus#FORBIDDEN}.
@ -1209,6 +1258,7 @@ public class ServerHttpSecurity {
public CsrfSpec requireCsrfProtectionMatcher( public CsrfSpec requireCsrfProtectionMatcher(
ServerWebExchangeMatcher requireCsrfProtectionMatcher) { ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
this.filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher); this.filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
this.specifiedRequireCsrfProtectionMatcher = true;
return this; return this;
} }

View File

@ -48,6 +48,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.DispatcherHandler;
@ -160,6 +161,25 @@ public class OAuth2ResourceServerSpecTests {
.expectStatus().isOk(); .expectStatus().isOk();
} }
@Test
public void postWhenSignedThenReturnsOk() {
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
this.client.post()
.headers(headers -> headers.setBearerAuth(this.messageReadToken))
.exchange()
.expectStatus().isOk();
}
@Test
public void postWhenMissingTokenThenReturnsForbidden() {
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
this.client.post()
.exchange()
.expectStatus().isForbidden();
}
@Test @Test
public void getJwtDecoderWhenBeanWiredAndDslWiredThenDslTakesPrecedence() { public void getJwtDecoderWhenBeanWiredAndDslWiredThenDslTakesPrecedence() {
GenericWebApplicationContext context = autowireWebServerGenericWebApplicationContext(); GenericWebApplicationContext context = autowireWebServerGenericWebApplicationContext();
@ -301,7 +321,12 @@ public class OAuth2ResourceServerSpecTests {
@RestController @RestController
static class RootController { static class RootController {
@GetMapping @GetMapping
Mono<String> root() { Mono<String> get() {
return Mono.just("ok");
}
@PostMapping
Mono<String> post() {
return Mono.just("ok"); return Mono.just("ok");
} }
} }