Skip to content

Commit 68d836d

Browse files
jzheauxrwinch
authored andcommitted
Reactive Resource Server Csrf Bypass
This makes requests identified as bearer token requests skip the csrf filter. Fixes: gh-5710
1 parent 820fb7d commit 68d836d

File tree

2 files changed

+77
-2
lines changed

2 files changed

+77
-2
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
4444
import org.springframework.security.authorization.AuthorizationDecision;
4545
import org.springframework.security.authorization.ReactiveAuthorizationManager;
46+
import org.springframework.security.core.Authentication;
4647
import org.springframework.security.core.AuthenticationException;
4748
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
4849
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
@@ -114,7 +115,9 @@
114115
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
115116
import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter;
116117
import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter;
118+
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
117119
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
120+
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
118121
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
119122
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
120123
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
@@ -130,6 +133,8 @@
130133
import org.springframework.web.server.WebFilterChain;
131134

132135
import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
136+
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match;
137+
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch;
133138

134139
/**
135140
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux.
@@ -703,6 +708,9 @@ protected void configure(ServerHttpSecurity http) {
703708
public class JwtSpec {
704709
private ReactiveJwtDecoder jwtDecoder;
705710

711+
private BearerTokenServerWebExchangeMatcher bearerTokenServerWebExchangeMatcher =
712+
new BearerTokenServerWebExchangeMatcher();
713+
706714
/**
707715
* Configures the {@link ReactiveJwtDecoder} to use
708716
* @param jwtDecoder the decoder to use
@@ -740,13 +748,20 @@ public OAuth2ResourceServerSpec and() {
740748
}
741749

742750
protected void configure(ServerHttpSecurity http) {
751+
ServerBearerTokenAuthenticationConverter bearerTokenConverter =
752+
new ServerBearerTokenAuthenticationConverter();
753+
this.bearerTokenServerWebExchangeMatcher.setBearerTokenConverter(bearerTokenConverter);
754+
755+
registerDefaultCsrfOverride(http);
756+
743757
BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
744758
ReactiveJwtDecoder jwtDecoder = getJwtDecoder();
745759
JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager(
746760
jwtDecoder);
747761
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
748-
oauth2.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
762+
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
749763
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
764+
750765
http
751766
.exceptionHandling()
752767
.authenticationEntryPoint(entryPoint)
@@ -760,6 +775,38 @@ protected ReactiveJwtDecoder getJwtDecoder() {
760775
}
761776
return this.jwtDecoder;
762777
}
778+
779+
private void registerDefaultCsrfOverride(ServerHttpSecurity http) {
780+
if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) {
781+
http
782+
.csrf()
783+
.requireCsrfProtectionMatcher(
784+
new AndServerWebExchangeMatcher(
785+
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
786+
new NegatedServerWebExchangeMatcher(
787+
this.bearerTokenServerWebExchangeMatcher)));
788+
}
789+
}
790+
791+
private class BearerTokenServerWebExchangeMatcher implements ServerWebExchangeMatcher {
792+
ServerBearerTokenAuthenticationConverter bearerTokenConverter;
793+
794+
@Override
795+
public Mono<MatchResult> matches(ServerWebExchange exchange) {
796+
return this.bearerTokenConverter.convert(exchange)
797+
.flatMap(this::nullAuthentication)
798+
.onErrorResume(e -> notMatch());
799+
}
800+
801+
public void setBearerTokenConverter(ServerBearerTokenAuthenticationConverter bearerTokenConverter) {
802+
Assert.notNull(bearerTokenConverter, "bearerTokenConverter cannot be null");
803+
this.bearerTokenConverter = bearerTokenConverter;
804+
}
805+
806+
private Mono<MatchResult> nullAuthentication(Authentication authentication) {
807+
return authentication == null ? notMatch() : match();
808+
}
809+
}
763810
}
764811

765812
public ServerHttpSecurity and() {
@@ -1173,6 +1220,8 @@ public AuthorizeExchangeSpec access(ReactiveAuthorizationManager<AuthorizationCo
11731220
public class CsrfSpec {
11741221
private CsrfWebFilter filter = new CsrfWebFilter();
11751222

1223+
private boolean specifiedRequireCsrfProtectionMatcher;
1224+
11761225
/**
11771226
* Configures the {@link ServerAccessDeniedHandler} used when a CSRF token is invalid. Default is
11781227
* to send an {@link org.springframework.http.HttpStatus#FORBIDDEN}.
@@ -1209,6 +1258,7 @@ public CsrfSpec csrfTokenRepository(
12091258
public CsrfSpec requireCsrfProtectionMatcher(
12101259
ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
12111260
this.filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
1261+
this.specifiedRequireCsrfProtectionMatcher = true;
12121262
return this;
12131263
}
12141264

config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.test.context.junit4.SpringRunner;
4949
import org.springframework.test.web.reactive.server.WebTestClient;
5050
import org.springframework.web.bind.annotation.GetMapping;
51+
import org.springframework.web.bind.annotation.PostMapping;
5152
import org.springframework.web.bind.annotation.RestController;
5253
import org.springframework.web.context.support.GenericWebApplicationContext;
5354
import org.springframework.web.reactive.DispatcherHandler;
@@ -160,6 +161,25 @@ public void getWhenUsingJwkSetUriThenConsultsAccordingly() {
160161
.expectStatus().isOk();
161162
}
162163

164+
@Test
165+
public void postWhenSignedThenReturnsOk() {
166+
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
167+
168+
this.client.post()
169+
.headers(headers -> headers.setBearerAuth(this.messageReadToken))
170+
.exchange()
171+
.expectStatus().isOk();
172+
}
173+
174+
@Test
175+
public void postWhenMissingTokenThenReturnsForbidden() {
176+
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
177+
178+
this.client.post()
179+
.exchange()
180+
.expectStatus().isForbidden();
181+
}
182+
163183
@Test
164184
public void getJwtDecoderWhenBeanWiredAndDslWiredThenDslTakesPrecedence() {
165185
GenericWebApplicationContext context = autowireWebServerGenericWebApplicationContext();
@@ -301,7 +321,12 @@ ReactiveJwtDecoder jwtDecoder() {
301321
@RestController
302322
static class RootController {
303323
@GetMapping
304-
Mono<String> root() {
324+
Mono<String> get() {
325+
return Mono.just("ok");
326+
}
327+
328+
@PostMapping
329+
Mono<String> post() {
305330
return Mono.just("ok");
306331
}
307332
}

0 commit comments

Comments
 (0)