1
0
mirror of synced 2026-07-20 01:55:08 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 588e35f16d Bump Gradle Wrapper from 8.10.1 to 8.10.2 2024-09-25 13:26:17 -05:00
82 changed files with 155 additions and 2833 deletions
@@ -18,7 +18,6 @@ package org.springframework.security.cas.userdetails;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apereo.cas.client.validation.Assertion;
@@ -74,8 +73,7 @@ public final class GrantedAuthorityFromAssertionAttributesUserDetailsService
}
private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) {
return new SimpleGrantedAuthority(
this.convertToUpperCase ? o.toString().toUpperCase(Locale.ROOT) : o.toString());
return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString());
}
/**
@@ -171,8 +171,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
Assert.state(this.mappingCount > 0,
"At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())");
ObservationRegistry registry = getObservationRegistry();
AuthorizationManager<HttpServletRequest> manager = postProcess(
(AuthorizationManager<HttpServletRequest>) this.managerBuilder.build());
RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build());
if (registry.isNoop()) {
return manager;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.
@@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
@@ -58,9 +57,7 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
Assert.hasText(issuer, "Provider issuer cannot be null");
this.issuer = issuer;
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
}
@Override
@@ -21,7 +21,6 @@ import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.ApplicationContext;
@@ -34,8 +33,6 @@ import org.springframework.security.web.reactive.result.view.CsrfRequestDataValu
import org.springframework.security.web.server.ObservationWebFilterChainDecorator;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.result.view.AbstractView;
@@ -82,14 +79,11 @@ class WebFluxSecurityConfiguration {
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
WebFilterChainProxy springSecurityWebFilterChainFilter(ObjectProvider<ServerWebExchangeFirewall> firewall,
ObjectProvider<ServerExchangeRejectedHandler> rejectedHandler) {
WebFilterChainProxy springSecurityWebFilterChainFilter() {
WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains());
if (!this.observationRegistry.isNoop()) {
proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry));
}
firewall.ifUnique(proxy::setFirewall);
rejectedHandler.ifUnique(proxy::setExchangeRejectedHandler);
return proxy;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -18,7 +18,6 @@ package org.springframework.security.config.http;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.ServletRequest;
@@ -314,7 +313,7 @@ class HttpConfigurationBuilder {
// Needed to account for placeholders
static String createPath(String path, boolean lowerCase) {
return lowerCase ? path.toLowerCase(Locale.ENGLISH) : path;
return lowerCase ? path.toLowerCase() : path;
}
BeanMetadataElement getSecurityContextHolderStrategyForAuthenticationFilters() {
@@ -147,7 +147,6 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
.addConstructorArgValue(logoutRequestResolver)
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
.getBeanDefinition();
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)
@@ -1,65 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.config.web.server;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.ClassUtils;
/**
* Utility methods for {@link HttpMessageConverter}'s.
*
* @author Joe Grandja
* @author luamas
* @since 5.1
*/
final class HttpMessageConverters {
private static final boolean jackson2Present;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
static {
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
}
private HttpMessageConverters() {
}
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
if (jackson2Present) {
return new MappingJackson2HttpMessageConverter();
}
if (gsonPresent) {
return new GsonHttpMessageConverter();
}
if (jsonbPresent) {
return new JsonbHttpMessageConverter();
}
return null;
}
}
@@ -1,95 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.config.web.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageEncoder;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.MimeType;
class OAuth2ErrorEncoder implements HttpMessageEncoder<OAuth2Error> {
private final HttpMessageConverter<Object> messageConverter = HttpMessageConverters.getJsonMessageConverter();
@NotNull
@Override
public List<MediaType> getStreamingMediaTypes() {
return List.of();
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
return getEncodableMimeTypes().contains(mimeType);
}
@NotNull
@Override
public Flux<DataBuffer> encode(Publisher<? extends OAuth2Error> error, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return Mono.from(error).flatMap((data) -> {
ByteArrayHttpOutputMessage bytes = new ByteArrayHttpOutputMessage();
try {
this.messageConverter.write(data, MediaType.APPLICATION_JSON, bytes);
return Mono.just(bytes.getBody().toByteArray());
}
catch (IOException ex) {
return Mono.error(ex);
}
}).map(bufferFactory::wrap).flux();
}
@NotNull
@Override
public List<MimeType> getEncodableMimeTypes() {
return List.of(MediaType.APPLICATION_JSON);
}
private static class ByteArrayHttpOutputMessage implements HttpOutputMessage {
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
@NotNull
@Override
public ByteArrayOutputStream getBody() {
return this.body;
}
@NotNull
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.
@@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
@@ -58,9 +57,7 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
Assert.hasText(issuer, "Provider issuer cannot be null");
this.issuer = issuer;
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
}
@Override
@@ -16,17 +16,16 @@
package org.springframework.security.config.web.server;
import java.util.Collections;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
@@ -63,9 +62,6 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
new OAuth2ErrorEncoder());
/**
* Construct an {@link OidcBackChannelLogoutWebFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
@@ -88,7 +84,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
})
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap(this.authenticationManager::authenticate)
@@ -97,7 +93,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
})
.flatMap((authentication) -> {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
@@ -105,12 +101,19 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
});
}
private Mono<Void> handleAuthenticationFailure(ServerWebExchange exchange, Exception ex) {
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return this.errorHttpMessageConverter.write(Mono.just(oauth2Error(ex)), ResolvableType.forClass(Object.class),
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
exchange.getResponse(), Collections.emptyMap());
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
OAuth2Error error = oauth2Error(ex);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
}
private OAuth2Error oauth2Error(Exception ex) {
@@ -16,8 +16,8 @@
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -25,15 +25,14 @@ import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
@@ -45,7 +44,6 @@ import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -65,9 +63,6 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
new OAuth2ErrorEncoder());
private WebClient web = WebClient.create();
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
@@ -102,7 +97,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
totalCount.intValue()));
}
if (!list.isEmpty()) {
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
}
else {
return Mono.empty();
@@ -153,11 +148,17 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
private Mono<Void> handleLogoutFailure(ServerWebExchange exchange, OAuth2Error error) {
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return this.errorHttpMessageConverter.write(Mono.just(error), ResolvableType.forClass(Object.class),
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
exchange.getResponse(), Collections.emptyMap());
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
}
/**
@@ -153,7 +153,6 @@ public class AuthorizeHttpRequestsConfigurerTests {
this.spring.register(ObjectPostProcessorConfig.class).autowire();
ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class);
verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class));
verify(objectPostProcessor).postProcess(any(AuthorizationManager.class));
verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class));
}
@@ -91,7 +91,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willThrow;
@@ -260,22 +259,6 @@ public class OidcLogoutConfigurerTests {
verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class));
}
@Test
void logoutWhenProviderIssuerMissingThenThrowIllegalArgumentException() throws Exception {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
.autowire();
String registrationId = this.clientRegistration.getRegistrationId();
MockHttpSession session = login();
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
assertThatIllegalArgumentException().isThrownBy(
() -> this.mvc.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
.param("logout_token", logoutToken)));
}
private MockHttpSession login() throws Exception {
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
@@ -427,54 +410,6 @@ public class OidcLogoutConfigurerTests {
}
@Configuration
static class ProviderIssuerMissingRegistrationConfig {
@Autowired(required = false)
MockWebServer web;
@Bean
ClientRegistration clientRegistration() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
}
String issuer = this.web.url("/").toString();
return TestClientRegistrations.clientRegistration()
.issuerUri(null)
.jwkSetUri(issuer + "jwks")
.tokenUri(issuer + "token")
.userInfoUri(issuer + "user")
.scope("openid")
.build();
}
@Bean
ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
return new InMemoryClientRegistrationRepository(clientRegistration);
}
}
@Configuration
@EnableWebSecurity
@Import(ProviderIssuerMissingRegistrationConfig.class)
static class ProviderIssuerMissingConfig {
@Bean
@Order(1)
SecurityFilterChain filters(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
@@ -513,9 +448,6 @@ public class OidcLogoutConfigurerTests {
@Autowired
ClientRegistration registration;
@Autowired(required = false)
MockWebServer web;
@Bean
@Order(0)
SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception {
@@ -552,7 +484,7 @@ public class OidcLogoutConfigurerTests {
HttpSession session = request.getSession();
JwtEncoderParameters parameters = JwtEncoderParameters
.from(JwtClaimsSet.builder().id("id").subject(this.username)
.issuer(getIssuerUri()).issuedAt(Instant.now())
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
String token = this.encoder.encode(parameters).getTokenValue();
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
@@ -560,7 +492,7 @@ public class OidcLogoutConfigurerTests {
}
String idToken(String sessionId) {
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
.claim(LogoutTokenClaimNames.SID, sessionId).build();
@@ -569,13 +501,6 @@ public class OidcLogoutConfigurerTests {
return this.encoder.encode(parameters).getTokenValue();
}
private String getIssuerUri() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
}
return this.web.url("/").toString();
}
@GetMapping("/user")
Map<String, Object> userinfo() {
return Map.of("sub", this.username, "id", this.username);
@@ -484,7 +484,6 @@ public class Saml2LogoutConfigurerTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
@@ -16,26 +16,14 @@
package org.springframework.security.config.annotation.web.reactive;
import java.util.Collections;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
@@ -59,46 +47,6 @@ public class WebFluxSecurityConfigurationTests {
assertThat(webFilterChainProxy).isNotNull();
}
@Test
void loadConfigWhenDefaultThenFirewalled() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
void loadConfigWhenCustomRejectedHandler() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class, CustomServerExchangeRejectedHandlerConfig.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode())
.isEqualTo(CustomServerExchangeRejectedHandlerConfig.EXPECTED_STATUS);
}
@Test
void loadConfigWhenFirewallBeanThenCustomized() throws Exception {
this.spring
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
WebFluxSecurityConfiguration.class, NoOpFirewallConfig.class)
.autowire();
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
DefaultWebFilterChain chain = emptyChain();
webFilterChainProxy.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isNotEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void loadConfigWhenBeanProxyingEnabledAndSubclassThenWebFilterChainProxyExists() {
this.spring
@@ -109,32 +57,6 @@ public class WebFluxSecurityConfigurationTests {
assertThat(webFilterChainProxy).isNotNull();
}
private static @NotNull DefaultWebFilterChain emptyChain() {
return new DefaultWebFilterChain((webExchange) -> Mono.empty(), Collections.emptyList());
}
@Configuration
static class NoOpFirewallConfig {
@Bean
ServerWebExchangeFirewall noOpFirewall() {
return ServerWebExchangeFirewall.INSECURE_NOOP;
}
}
@Configuration
static class CustomServerExchangeRejectedHandlerConfig {
static HttpStatus EXPECTED_STATUS = HttpStatus.I_AM_A_TEAPOT;
@Bean
ServerExchangeRejectedHandler rejectedHandler() {
return new HttpStatusExchangeRejectedHandler(EXPECTED_STATUS);
}
}
@Configuration
static class SubclassConfig extends WebFluxSecurityConfiguration {
@@ -63,7 +63,6 @@ import org.springframework.web.util.UriUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
@@ -381,22 +380,6 @@ public class Saml2LogoutBeanDefinitionParserTests {
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
}
// gh-11363
@Test
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
this.spring.configLocations(this.xml("CustomComponents")).autowire();
RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
.samlRequest(this.rpLogoutRequest)
.id(this.rpLogoutRequestId)
.relayState(this.rpLogoutRequestRelayState)
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature))
.build();
given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()));
verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any());
}
private <T> T getBean(Class<T> clazz) {
return this.spring.getContext().getBean(clazz);
}
@@ -50,7 +50,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -98,7 +97,6 @@ import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
@@ -197,10 +195,7 @@ public class OidcLogoutSpecTests {
.body(BodyInserters.fromFormData("logout_token", "invalid"))
.exchange()
.expectStatus()
.isBadRequest()
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
})
.value(hasValue("invalid_request"));
.isBadRequest();
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
}
@@ -267,10 +262,9 @@ public class OidcLogoutSpecTests {
.exchange()
.expectStatus()
.isBadRequest()
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
})
.value(hasValue("partial_logout"))
.value(hasValue(containsString("not all sessions were terminated")));
.expectBody(String.class)
.value(containsString("partial_logout"))
.value(containsString("not all sessions were terminated"));
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
}
@@ -329,30 +323,6 @@ public class OidcLogoutSpecTests {
verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class));
}
@Test
void logoutWhenProviderIssuerMissingThen5xxServerError() {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
.autowire();
String registrationId = this.clientRegistration.getRegistrationId();
String session = login();
String logoutToken = this.test.mutateWith(session(session))
.get()
.uri("/token/logout")
.exchange()
.expectStatus()
.isOk()
.returnResult(String.class)
.getResponseBody()
.blockFirst();
this.test.post()
.uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
.body(BodyInserters.fromFormData("logout_token", logoutToken))
.exchange()
.expectStatus()
.is5xxServerError();
this.test.mutateWith(session(session)).get().uri("/token/logout").exchange().expectStatus().isOk();
}
private String login() {
this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized();
String registrationId = this.clientRegistration.getRegistrationId();
@@ -529,54 +499,6 @@ public class OidcLogoutSpecTests {
}
@Configuration
static class ProviderIssuerMissingRegistrationConfig {
@Autowired(required = false)
MockWebServer web;
@Bean
ClientRegistration clientRegistration() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
}
String issuer = this.web.url("/").toString();
return TestClientRegistrations.clientRegistration()
.issuerUri(null)
.jwkSetUri(issuer + "jwks")
.tokenUri(issuer + "token")
.userInfoUri(issuer + "user")
.scope("openid")
.build();
}
@Bean
ReactiveClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}
}
@Configuration
@EnableWebFluxSecurity
@Import(ProviderIssuerMissingRegistrationConfig.class)
static class ProviderIssuerMissingConfig {
@Bean
@Order(1)
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebFluxSecurity
@EnableWebFlux
@@ -615,9 +537,6 @@ public class OidcLogoutSpecTests {
@Autowired
ClientRegistration registration;
@Autowired(required = false)
MockWebServer web;
static ServerWebExchangeMatcher or(String... patterns) {
List<ServerWebExchangeMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
@@ -662,7 +581,7 @@ public class OidcLogoutSpecTests {
Map<String, Object> accessToken(WebSession session) {
JwtEncoderParameters parameters = JwtEncoderParameters
.from(JwtClaimsSet.builder().id("id").subject(this.username)
.issuer(getIssuerUri()).issuedAt(Instant.now())
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
String token = this.encoder.encode(parameters).getTokenValue();
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
@@ -670,7 +589,7 @@ public class OidcLogoutSpecTests {
}
String idToken(String sessionId) {
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
.claim(LogoutTokenClaimNames.SID, sessionId).build();
@@ -679,13 +598,6 @@ public class OidcLogoutSpecTests {
return this.encoder.encode(parameters).getTokenValue();
}
private String getIssuerUri() {
if (this.web == null) {
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
}
return this.web.url("/").toString();
}
@GetMapping("/user")
Map<String, Object> userinfo() {
return Map.of("sub", this.username, "id", this.username);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -16,8 +16,6 @@
package org.springframework.security.authentication;
import java.util.Locale;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
@@ -55,7 +53,7 @@ public final class AuthenticationObservationConvention
if (authenticationType.endsWith("Authentication")) {
authenticationType = authenticationType.substring(0, authenticationType.lastIndexOf("Authentication"));
}
return "authenticate " + authenticationType.toLowerCase(Locale.ENGLISH);
return "authenticate " + authenticationType.toLowerCase();
}
return "authenticate";
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* 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.
@@ -79,10 +79,10 @@ public class SimpleAttributes2GrantedAuthoritiesMapper
*/
private GrantedAuthority getGrantedAuthority(String attribute) {
if (isConvertAttributeToLowerCase()) {
attribute = attribute.toLowerCase(Locale.ROOT);
attribute = attribute.toLowerCase(Locale.getDefault());
}
else if (isConvertAttributeToUpperCase()) {
attribute = attribute.toUpperCase(Locale.ROOT);
attribute = attribute.toUpperCase(Locale.getDefault());
}
if (isAddPrefixIfAlreadyExisting() || !attribute.startsWith(getAttributePrefix())) {
return new SimpleGrantedAuthority(getAttributePrefix() + attribute);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* 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.
@@ -18,7 +18,6 @@ package org.springframework.security.core.authority.mapping;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.springframework.beans.factory.InitializingBean;
@@ -72,10 +71,10 @@ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, In
private GrantedAuthority mapAuthority(String name) {
if (this.convertToUpperCase) {
name = name.toUpperCase(Locale.ROOT);
name = name.toUpperCase();
}
else if (this.convertToLowerCase) {
name = name.toLowerCase(Locale.ROOT);
name = name.toLowerCase();
}
if (this.prefix.length() > 0 && !name.startsWith(this.prefix)) {
name = this.prefix + name;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2017 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.
@@ -18,7 +18,6 @@ package org.springframework.security.core.userdetails;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -92,7 +91,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService
}
private String getKey(String username) {
return username.toLowerCase(Locale.ROOT);
return username.toLowerCase();
}
}
@@ -19,7 +19,6 @@ package org.springframework.security.core.userdetails.memory;
import java.beans.PropertyEditorSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.util.StringUtils;
@@ -46,10 +45,10 @@ public class UserAttributeEditor extends PropertyEditorSupport {
userAttrib.setPassword(currentToken);
}
else {
if (currentToken.toLowerCase(Locale.ENGLISH).equals("enabled")) {
if (currentToken.toLowerCase().equals("enabled")) {
userAttrib.setEnabled(true);
}
else if (currentToken.toLowerCase(Locale.ENGLISH).equals("disabled")) {
else if (currentToken.toLowerCase().equals("disabled")) {
userAttrib.setEnabled(false);
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -19,7 +19,6 @@ package org.springframework.security.provisioning;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
@@ -97,23 +96,23 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
@Override
public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
@Override
public void deleteUser(String username) {
this.users.remove(username.toLowerCase(Locale.ROOT));
this.users.remove(username.toLowerCase());
}
@Override
public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
@Override
public boolean userExists(String username) {
return this.users.containsKey(username.toLowerCase(Locale.ROOT));
return this.users.containsKey(username.toLowerCase());
}
@Override
@@ -144,14 +143,14 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = this.users.get(username.toLowerCase(Locale.ROOT));
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
mutableUser.setPassword(newPassword);
return mutableUser;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = this.users.get(username.toLowerCase(Locale.ROOT));
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2018 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.
@@ -18,7 +18,6 @@ package org.springframework.security.crypto.password;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Locale;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
@@ -51,11 +50,11 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
private static final String SSHA_PREFIX = "{SSHA}";
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(Locale.ENGLISH);
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
private static final String SHA_PREFIX = "{SHA}";
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(Locale.ENGLISH);
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
private BytesKeyGenerator saltGenerator;
+1 -1
View File
@@ -30,7 +30,7 @@ urls:
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
snapshot: true
runtime:
log:
-1
View File
@@ -153,7 +153,6 @@
*** xref:reactive/exploits/csrf.adoc[CSRF]
*** xref:reactive/exploits/headers.adoc[Headers]
*** xref:reactive/exploits/http.adoc[HTTP Requests]
*** xref:reactive/exploits/firewall.adoc[]
** Integrations
*** xref:reactive/integrations/cors.adoc[CORS]
*** xref:reactive/integrations/rsocket.adoc[RSocket]
@@ -1,234 +0,0 @@
[[webflux-serverwebexchangefirewall]]
= ServerWebExchangeFirewall
There are various ways a request can be created by malicious users that can exploit applications.
Spring Security provides the `ServerWebExchangeFirewall` to allow rejecting requests that look malicious.
The default implementation is `StrictServerWebExchangeFirewall` which rejects malicious requests.
For example a request could contain path-traversal sequences (such as `/../`) or multiple forward slashes (`//`) that could also cause pattern-matches to fail.
Some containers normalize these out before performing the servlet mapping, but others do not.
To protect against issues like these, `WebFilterChainProxy` uses a `ServerWebExchangeFirewall` strategy to check and wrap the request.
By default, un-normalized requests are automatically rejected, and path parameters are removed for matching purposes.
(So, for example, an original request path of `/secure;hack=1/somefile.html;hack=2` is returned as `/secure/somefile.html`.)
It is, therefore, essential that a `WebFilterChainProxy` is used.
In practice, we recommend that you use method security at your service layer, to control access to your application, rather than rely entirely on the use of security constraints defined at the web-application level.
URLs change, and it is difficult to take into account all the possible URLs that an application might support and how requests might be manipulated.
You should restrict yourself to using a few simple patterns that are simple to understand.
Always try to use a "`deny-by-default`" approach, where you have a catch-all wildcard (`/**` or `**`) defined last to deny access.
Security defined at the service layer is much more robust and harder to bypass, so you should always take advantage of Spring Security's method security options.
You can customize the `ServerWebExchangeFirewall` by exposing it as a Bean.
.Allow Matrix Variables
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowSemicolon(true);
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowSemicolon(true)
return firewall
}
----
======
To protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering], the `StrictServerWebExchangeFirewall` provides an allowed list of valid HTTP methods that are allowed.
The default valid methods are `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, and `PUT`.
If your application needs to modify the valid methods, you can configure a custom `StrictServerWebExchangeFirewall` bean.
The following example allows only HTTP `GET` and `POST` methods:
.Allow Only GET & POST
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowedHttpMethods(listOf("GET", "POST"))
return firewall
}
----
======
If you must allow any HTTP method (not recommended), you can use `StrictServerWebExchangeFirewall.setUnsafeAllowAnyHttpMethod(true)`.
Doing so entirely disables validation of the HTTP method.
[[webflux-serverwebexchangefirewall-headers-parameters]]
`StrictServerWebExchangeFirewall` also checks header names and values and parameter names.
It requires that each character have a defined code point and not be a control character.
This requirement can be relaxed or adjusted as necessary by using the following methods:
* `StrictServerWebExchangeFirewall#setAllowedHeaderNames(Predicate)`
* `StrictServerWebExchangeFirewall#setAllowedHeaderValues(Predicate)`
* `StrictServerWebExchangeFirewall#setAllowedParameterNames(Predicate)`
[NOTE]
====
Parameter values can be also controlled with `setAllowedParameterValues(Predicate)`.
====
For example, to switch off this check, you can wire your `StrictServerWebExchangeFirewall` with `Predicate` instances that always return `true`:
.Allow Any Header Name, Header Value, and Parameter Name
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowedHeaderNames((header) -> true);
firewall.setAllowedHeaderValues((header) -> true);
firewall.setAllowedParameterNames((parameter) -> true);
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowedHeaderNames { true }
firewall.setAllowedHeaderValues { true }
firewall.setAllowedParameterNames { true }
return firewall
}
----
======
Alternatively, there might be a specific value that you need to allow.
For example, iPhone Xʀ uses a `User-Agent` that includes a character that is not in the ISO-8859-1 charset.
Due to this fact, some application servers parse this value into two separate characters, the latter being an undefined character.
You can address this with the `setAllowedHeaderValues` method:
.Allow Certain User Agents
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
Pattern allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
Pattern userAgent = ...;
firewall.setAllowedHeaderValues((header) -> allowed.matcher(header).matches() || userAgent.matcher(header).matches());
return firewall;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
val allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*")
val userAgent = Pattern.compile(...)
firewall.setAllowedHeaderValues { allowed.matcher(it).matches() || userAgent.matcher(it).matches() }
return firewall
}
----
======
In the case of header values, you may instead consider parsing them as UTF-8 at verification time:
.Parse Headers As UTF-8
[tabs]
======
Java::
+
[source,java,role="primary"]
----
firewall.setAllowedHeaderValues((header) -> {
String parsed = new String(header.getBytes(ISO_8859_1), UTF_8);
return allowed.matcher(parsed).matches();
});
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
firewall.setAllowedHeaderValues {
val parsed = String(header.getBytes(ISO_8859_1), UTF_8)
return allowed.matcher(parsed).matches()
}
----
======
The `ServerExchangeRejectedHandler` interface is used to handle `ServerExchangeRejectedException` throw by Spring Security's `ServerWebExchangeFirewall`.
By default `HttpStatusExchangeRejectedHandler` is used to send an HTTP 400 response to clients when a request is rejected.
To customize the behavior, users can expose a `ServerExchangeRejectedHandler` Bean.
For example, the following will send an HTTP 404 when the request is rejected:
.Send 404 on Request Rejected
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ServerExchangeRejectedHandler rejectedHandler() {
return new HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND);
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun rejectedHandler(): ServerExchangeRejectedHandler {
return HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND)
}
----
======
Handling can be completely customized by creating a custom `ServerExchangeRejectedHandler` implementation.
@@ -14,7 +14,6 @@ The following image shows the typical layering of the handlers for a single HTTP
.FilterChain
[[servlet-filterchain-figure]]
[.invert-dark]
image::{figures}/filterchain.png[]
The client sends a request to the application, and the container creates a `FilterChain`, which contains the `Filter` instances and `Servlet` that should process the `HttpServletRequest`, based on the path of the request URI.
@@ -67,7 +66,6 @@ Here is a picture of how `DelegatingFilterProxy` fits into the <<servlet-filters
.DelegatingFilterProxy
[[servlet-delegatingfilterproxy-figure]]
[.invert-dark]
image::{figures}/delegatingfilterproxy.png[]
`DelegatingFilterProxy` looks up __Bean Filter~0~__ from the `ApplicationContext` and then invokes __Bean Filter~0~__.
@@ -115,7 +113,6 @@ The following image shows the role of `FilterChainProxy`.
.FilterChainProxy
[[servlet-filterchainproxy-figure]]
[.invert-dark]
image::{figures}/filterchainproxy.png[]
[[servlet-securityfilterchain]]
@@ -127,7 +124,6 @@ The following image shows the role of `SecurityFilterChain`.
.SecurityFilterChain
[[servlet-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/securityfilterchain.png[]
The <<servlet-security-filters,Security Filters>> in `SecurityFilterChain` are typically Beans, but they are registered with `FilterChainProxy` instead of <<servlet-delegatingfilterproxy>>.
@@ -149,7 +145,6 @@ The following image shows multiple `SecurityFilterChain` instances:
.Multiple SecurityFilterChain
[[servlet-multi-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/multi-securityfilterchain.png[]
In the <<servlet-multi-securityfilterchain-figure>> figure, `FilterChainProxy` decides which `SecurityFilterChain` should be used.
@@ -403,7 +398,6 @@ The {security-api-url}org/springframework/security/web/access/ExceptionTranslati
The following image shows the relationship of `ExceptionTranslationFilter` to other components:
[.invert-dark]
image::{figures}/exceptiontranslationfilter.png[]
@@ -22,7 +22,6 @@ This also gives a good idea of the high level flow of authentication and how pie
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
It contains the <<servlet-authentication-securitycontext>>.
[.invert-dark]
image::{figures}/securitycontextholder.png[]
The `SecurityContextHolder` is where Spring Security stores the details of who is xref:features/authentication/index.adoc#authentication[authenticated].
@@ -176,7 +175,6 @@ While the implementation of `AuthenticationManager` could be anything, the most
Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide.
If none of the configured `AuthenticationProvider` instances can authenticate, authentication fails with a `ProviderNotFoundException`, which is a special `AuthenticationException` that indicates that the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it.
[.invert-dark]
image::{figures}/providermanager.png[]
In practice each `AuthenticationProvider` knows how to perform a specific type of authentication.
@@ -186,13 +184,11 @@ This lets each `AuthenticationProvider` do a very specific type of authenticatio
`ProviderManager` also allows configuring an optional parent `AuthenticationManager`, which is consulted in the event that no `AuthenticationProvider` can perform authentication.
The parent can be any type of `AuthenticationManager`, but it is often an instance of `ProviderManager`.
[.invert-dark]
image::{figures}/providermanager-parent.png[]
In fact, multiple `ProviderManager` instances might share the same parent `AuthenticationManager`.
This is somewhat common in scenarios where there are multiple xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] instances that have some authentication in common (the shared parent `AuthenticationManager`), but also different authentication mechanisms (the different `ProviderManager` instances).
[.invert-dark]
image::{figures}/providermanagers-parent.png[]
[[servlet-authentication-providermanager-erasing-credentials]]
@@ -238,7 +234,6 @@ Before the credentials can be authenticated, Spring Security typically requests
Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it.
[.invert-dark]
image::{figures}/abstractauthenticationprocessingfilter.png[]
image:{icondir}/number_1.png[] When the user submits their credentials, the `AbstractAuthenticationProcessingFilter` creates an <<servlet-authentication-authentication,`Authentication`>> from the `HttpServletRequest` to be authenticated.
@@ -9,7 +9,6 @@ This section describes how HTTP Basic Authentication works within Spring Securit
First, we see the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/basicauthenticationentrypoint.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -27,7 +26,6 @@ The following image shows the flow for the username and password being processed
[[servlet-authentication-basicauthenticationfilter]]
.Authenticating Username and Password
[.invert-dark]
image::{figures}/basicauthenticationfilter.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -8,7 +8,6 @@ This section examines how `DaoAuthenticationProvider` works within Spring Securi
The following figure explains the workings of the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section.
.`DaoAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/daoauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section passes a `UsernamePasswordAuthenticationToken` to the `AuthenticationManager`, which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -10,7 +10,6 @@ This section examines how form-based login works within Spring Security.
First, we see how the user is redirected to the login form:
.Redirecting to the Login Page
[.invert-dark]
image::{figures}/loginurlauthenticationentrypoint.png[]
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -31,7 +30,6 @@ When the username and password are submitted, the `UsernamePasswordAuthenticatio
The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so the following diagram should look pretty similar:
.Authenticating Username and Password
[.invert-dark]
image::{figures}/usernamepasswordauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -191,7 +191,6 @@ In Spring Security 6, the example shown above is the default configuration.
The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextpersistencefilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -213,7 +212,6 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `
The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextholderfilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -126,7 +126,6 @@ For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` a
[[authz-authorization-manager-implementations]]
.Authorization Manager Implementations
[.invert-dark]
image::{figures}/authorizationhierarchy.png[]
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
@@ -346,7 +345,6 @@ The following image shows the `AccessDecisionManager` interface:
[[authz-access-voting]]
.Voting Decision Manager
[.invert-dark]
image::{figures}/access-decision-voting.png[]
By using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
@@ -407,7 +405,6 @@ For example, on the Spring web site, you can find a https://spring.io/blog/2009/
[[authz-after-invocation]]
.After Invocation Implementation
[.invert-dark]
image::{figures}/after-invocation.png[]
Like many other parts of Spring Security, `AfterInvocationManager` has a single concrete implementation, `AfterInvocationProviderManager`, which polls a list of ``AfterInvocationProvider``s.
@@ -65,7 +65,6 @@ In many cases, your authorization rules will be more sophisticated than that, so
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications.
.Authorize HttpServletRequest
[.invert-dark]
image::{figures}/authorizationfilter.png[]
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
@@ -114,7 +114,6 @@ open class MyCustomerService {
A given invocation to `MyCustomerService#readCustomer` may look something like this when Method Security <<activate-method-security,is activated>>:
[.invert-dark]
image::{figures}/methodsecurity.png[]
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>>
@@ -82,7 +82,6 @@ To learn more about CSRF protection for your application, consider the following
CSRF protection is provided by several components that are composed within the {security-api-url}org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`]:
.`CsrfFilter` Components
[.invert-dark]
image::{figures}/csrf.png[]
CSRF protection is divided into two parts:
@@ -91,7 +90,6 @@ CSRF protection is divided into two parts:
2. Determine if the request requires CSRF protection, load and validate the token, and <<csrf-access-denied-handler,handle `AccessDeniedException`>>.
.`CsrfFilter` Processing
[.invert-dark]
image::{figures}/csrf-processing.png[]
* image:{icondir}/number_1.png[] First, the {security-api-url}org/springframework/security/web/csrf/DeferredCsrfToken.html[`DeferredCsrfToken`] is loaded, which holds a reference to the <<csrf-token-repository,`CsrfTokenRepository`>> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]).
@@ -205,78 +205,6 @@ This will ensure that:
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
[[migrating-spel-expressions]]
=== Migrating SpEL Expressions
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
However, to ease migration, you can also use a class like the following:
[source,java]
----
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
private Expression expression;
public MessageExpressionAuthorizationManager(String expressionString) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
return new ExpressionAuthorizationDecision(granted, this.expression);
}
}
----
And specify an instance for each matcher that you cannot get migrate:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
// ...
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
// ...
return messages.build();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
messages
// ..
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
// ...
return messages.build()
}
}
----
======
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
@@ -21,7 +21,6 @@ Now we can consider how Bearer Token Authentication works within Spring Security
First, we see that, as with xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/bearerauthenticationentrypoint.png[]
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -39,7 +38,6 @@ The following image shows the flow for the bearer token being processed:
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
.Authenticating Bearer Token
[.invert-dark]
image::{figures}/bearertokenauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -89,13 +89,12 @@ Next, let's see the architectural components that Spring Security uses to suppor
{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[`JwtAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <<oauth2resourceserver-jwt-decoder,`JwtDecoder`>> and <<oauth2resourceserver-jwt-authorization-extraction,`JwtAuthenticationConverter`>> to authenticate a JWT.
Let's take a look at how `JwtAuthenticationProvider` works within Spring Security.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> works.
.`JwtAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/jwtauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_1.png[] The authentication `Filter` from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `JwtAuthenticationProvider`.
@@ -85,13 +85,12 @@ Next, let's see the architectural components that Spring Security uses to suppor
{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[`OpaqueTokenAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <<oauth2resourceserver-opaque-introspector,`OpaqueTokenIntrospector`>> to authenticate an opaque token.
Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> works.
.`OpaqueTokenAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/opaquetokenauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_1.png[] The authentication `Filter` from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `OpaqueTokenAuthenticationProvider`.
@@ -7,7 +7,6 @@ First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes
It does this through a series of redirects:
.Redirecting to Asserting Party Authentication
[.invert-dark]
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
[NOTE]
@@ -35,7 +34,6 @@ The following image shows how Spring Security authenticates a `<saml2:Response>`
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
.Authenticating a `<saml2:Response>`
[.invert-dark]
image::{figures}/saml2webssoauthenticationfilter.png[]
[NOTE]
+2 -2
View File
@@ -1,8 +1,8 @@
{
"dependencies": {
"antora": "3.2.0-alpha.8",
"antora": "3.2.0-alpha.6",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.1",
"@antora/collector-extension": "1.0.0-beta.2",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
@@ -40,8 +40,4 @@
<!-- Lambdas that we can't replace with a method reference because a closure is required -->
<suppress files="BearerTokenAuthenticationFilter\.java" checks="SpringLambda"/>
<!-- Ignore String.toUpperCase() and String.toLowerCase() checks in tests -->
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
</suppressions>
-16
View File
@@ -30,21 +30,5 @@
<property name="message" value="Please use assertThatExceptionOfType." />
<property name="ignoreComments" value="true" />
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
<property name="id" value="toLowerCaseWithoutLocale"/>
<property name="format" value="\.toLowerCase\(\)"/>
<property name="maximum" value="0"/>
<property name="message"
value="String.toLowerCase() should be String.toLowerCase(Locale.ROOT) or String.toLowerCase(Locale.ENGLISH)"/>
<property name="ignoreComments" value="true"/>
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
<property name="id" value="toUpperCaseWithoutLocale"/>
<property name="format" value="\.toUpperCase\(\)"/>
<property name="maximum" value="0"/>
<property name="message"
value="String.toUpperCase() should be String.toUpperCase(Locale.ROOT) or String.toUpperCase(Locale.ENGLISH)"/>
<property name="ignoreComments" value="true"/>
</module>
</module>
</module>
+1 -1
View File
@@ -1,5 +1,5 @@
springBootVersion=3.1.1
version=6.2.9-SNAPSHOT
version=6.2.7-SNAPSHOT
samplesBranch=6.2.x
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+8 -8
View File
@@ -13,7 +13,7 @@ org-jetbrains-kotlin = "1.9.25"
org-jetbrains-kotlinx = "1.7.3"
org-mockito = "5.5.0"
org-opensaml = "4.3.2"
org-springframework = "6.1.15"
org-springframework = "6.1.13"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14"
@@ -26,9 +26,9 @@ com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebser
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.13"
io-mockk = "io.mockk:mockk:1.13.13"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.13"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.10"
io-mockk = "io.mockk:mockk:1.13.12"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.10"
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
@@ -68,11 +68,11 @@ org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", ve
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.3.2.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-junit-junit-bom = "org.junit:junit-bom:5.10.5"
org-junit-junit-bom = "org.junit:junit-bom:5.10.4"
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
org-opensaml-opensaml-core = { module = "org.opensaml:opensaml-core", version.ref = "org-opensaml" }
org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" }
@@ -84,8 +84,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.12"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.8"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.10"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.6"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+2 -1
View File
@@ -86,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -1,5 +1,5 @@
/*
* Copyright 2005-2024 the original author or authors.
* Copyright 2005-2010 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.
@@ -16,8 +16,6 @@
package org.springframework.security.ldap;
import java.util.Locale;
import org.springframework.ldap.BadLdapGrammarException;
/**
@@ -74,7 +72,7 @@ final class LdapEncoder {
}
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
String raw = Integer.toHexString(c).toUpperCase();
return (raw.length() > 1) ? raw : "0" + raw;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2005-2024 the original author or authors.
* Copyright 2005-2010 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.
@@ -16,8 +16,6 @@
package org.springframework.security.ldap.authentication;
import java.util.Locale;
import org.springframework.ldap.BadLdapGrammarException;
/**
@@ -74,7 +72,7 @@ final class LdapEncoder {
}
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
String raw = Integer.toHexString(c).toUpperCase();
return (raw.length() > 1) ? raw : "0" + raw;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* 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.
@@ -23,7 +23,6 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -145,9 +144,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.url = url;
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase(Locale.ROOT) : null;
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null;
}
/**
@@ -156,7 +155,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.url = url;
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null;
}
@@ -351,7 +350,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
}
String createBindPrincipal(String username) {
if (this.domain == null || username.toLowerCase(Locale.ROOT).endsWith(this.domain)) {
if (this.domain == null || username.toLowerCase().endsWith(this.domain)) {
return username;
}
return username + "@" + this.domain;
@@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -180,7 +179,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
return null;
}
if (this.convertToUpperCase) {
role = role.toUpperCase(Locale.ROOT);
role = role.toUpperCase();
}
return new SimpleGrantedAuthority(this.rolePrefix + role);
};
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -23,7 +23,6 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
@@ -125,7 +124,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
NamingEnumeration<?> ne = roleAttr.getAll();
Object group = ne.next();
String role = group.toString();
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase(Locale.ROOT));
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase());
};
private String[] attributesToRetrieve;
@@ -290,7 +289,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
@Deprecated
protected DistinguishedName buildGroupDn(String group) {
DistinguishedName dn = new DistinguishedName(this.groupSearchBase);
dn.add(this.groupRoleAttributeName, group.toLowerCase(Locale.ROOT));
dn.add(this.groupRoleAttributeName, group.toLowerCase());
return dn;
}
@@ -17,7 +17,6 @@
package org.springframework.security.ldap.userdetails;
import java.util.Collection;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -128,7 +127,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
protected GrantedAuthority createAuthority(Object role) {
if (role instanceof String) {
if (this.convertToUpperCase) {
role = ((String) role).toUpperCase(Locale.ROOT);
role = ((String) role).toUpperCase();
}
return new SimpleGrantedAuthority(this.rolePrefix + role);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2014 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.
@@ -18,7 +18,6 @@ package org.springframework.security.ldap.userdetails;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -194,7 +193,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
}
for (String role : roles) {
if (isConvertToUpperCase()) {
role = role.toUpperCase(Locale.ROOT);
role = role.toUpperCase();
}
role = getRolePrefix() + role;
// if the group already exist, we will not search for it's parents again.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2019 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.
@@ -80,13 +80,7 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
if (registration == null) {
return null;
}
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients
.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
if (cachedAuthorizedClient == null) {
return null;
}
return (T) new OAuth2AuthorizedClient(registration, cachedAuthorizedClient.getPrincipalName(),
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2019 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.
@@ -62,15 +62,8 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.mapNotNull((clientRegistration) -> {
OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName);
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id);
if (cachedAuthorizedClient == null) {
return null;
}
return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(),
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
});
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -18,7 +18,6 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
@@ -540,7 +539,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
// @formatter:off
return Stream.of(wwwAuthenticateHeader)
.filter((header) -> StringUtils.hasLength(header))
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
.filter((header) -> header.toLowerCase().startsWith("bearer"))
.map((header) -> header.substring("bearer".length()))
.map((header) -> header.split(","))
.flatMap(Stream::of)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -18,7 +18,6 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -655,7 +654,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
private Map<String, String> parseAuthParameters(String wwwAuthenticateHeader) {
// @formatter:off
return Stream.of(wwwAuthenticateHeader).filter((header) -> StringUtils.hasLength(header))
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
.filter((header) -> header.toLowerCase().startsWith("bearer"))
.map((header) -> header.substring("bearer".length()))
.map((header) -> header.split(","))
.flatMap(Stream::of)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2019 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.
@@ -18,25 +18,22 @@ package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatObject;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link InMemoryOAuth2AuthorizedClientService}.
@@ -82,11 +79,9 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
@Test
public void constructorWhenAuthorizedClientsProvidedThenUseProvidedAuthorizedClients() {
String registrationId = this.registration3.getRegistrationId();
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration3, this.principalName1,
mock(OAuth2AccessToken.class));
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients = Collections.singletonMap(
new OAuth2AuthorizedClientId(this.registration3.getRegistrationId(), this.principalName1),
authorizedClient);
mock(OAuth2AuthorizedClient.class));
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(eq(registrationId))).willReturn(this.registration3);
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
@@ -129,35 +124,7 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
}
@Test
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnAuthorizedClientWithUpdatedClientRegistration() {
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.registration1)
.clientSecret("updated secret")
.build();
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(this.registration1.getRegistrationId()))
.willReturn(this.registration1, updatedRegistration);
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
clientRegistrationRepository);
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.registration1,
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
authorizedClientService.saveAuthorizedClient(cachedAuthorizedClient,
new TestingAuthenticationToken(this.principalName1, null));
OAuth2AuthorizedClient authorizedClientWithUpdatedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
OAuth2AuthorizedClient firstLoadedClient = authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
OAuth2AuthorizedClient secondLoadedClient = authorizedClientService
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
assertThat(firstLoadedClient).satisfies(isEqualTo(cachedAuthorizedClient));
assertThat(secondLoadedClient).satisfies(isEqualTo(authorizedClientWithUpdatedRegistration));
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
}
@Test
@@ -181,7 +148,7 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
.loadAuthorizedClient(this.registration3.getRegistrationId(), this.principalName2);
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
}
@Test
@@ -213,38 +180,4 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
assertThat(loadedAuthorizedClient).isNull();
}
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
return (actual) -> {
assertThat(actual).isNotNull();
assertThat(actual.getClientRegistration().getRegistrationId())
.isEqualTo(expected.getClientRegistration().getRegistrationId());
assertThat(actual.getClientRegistration().getClientName())
.isEqualTo(expected.getClientRegistration().getClientName());
assertThat(actual.getClientRegistration().getRedirectUri())
.isEqualTo(expected.getClientRegistration().getRedirectUri());
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
assertThat(actual.getClientRegistration().getClientId())
.isEqualTo(expected.getClientRegistration().getClientId());
assertThat(actual.getClientRegistration().getClientSecret())
.isEqualTo(expected.getClientRegistration().getClientSecret());
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
if (expected.getRefreshToken() != null) {
assertThat(actual.getRefreshToken()).isNotNull();
assertThat(actual.getRefreshToken().getTokenValue())
.isEqualTo(expected.getRefreshToken().getTokenValue());
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
assertThat(actual.getRefreshToken().getExpiresAt())
.isEqualTo(expected.getRefreshToken().getExpiresAt());
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2020 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.
@@ -18,14 +18,12 @@ package org.springframework.security.oauth2.client;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -36,9 +34,7 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
@@ -60,9 +56,8 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused");
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(),
Instant.now().plus(Duration.ofDays(1)));
// @formatter:off
private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId)
@@ -84,11 +79,6 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
public void setup() {
this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(
this.clientRegistrationRepository);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofDays(1));
this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt);
this.refreshToken = new OAuth2RefreshToken("refresh", issuedAt, expiresAt);
}
@Test
@@ -163,37 +153,11 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
.saveAuthorizedClient(authorizedClient, this.principal)
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
StepVerifier.create(saveAndLoad)
.assertNext(isEqualTo(authorizedClient))
.expectNext(authorizedClient)
.verifyComplete();
// @formatter:on
}
@Test
@SuppressWarnings("unchecked")
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnsAuthorizedClientWithUpdatedClientRegistration() {
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.clientRegistration)
.clientSecret("updated secret")
.build();
given(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId))
.willReturn(Mono.just(this.clientRegistration), Mono.just(updatedRegistration));
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.clientRegistration,
this.principalName, this.accessToken, this.refreshToken);
OAuth2AuthorizedClient authorizedClientWithChangedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
this.principalName, this.accessToken, this.refreshToken);
Flux<OAuth2AuthorizedClient> saveAndLoadTwice = this.authorizedClientService
.saveAuthorizedClient(cachedAuthorizedClient, this.principal)
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
.concatWith(
this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
StepVerifier.create(saveAndLoadTwice)
.assertNext(isEqualTo(cachedAuthorizedClient))
.assertNext(isEqualTo(authorizedClientWithChangedRegistration))
.verifyComplete();
}
@Test
public void saveAuthorizedClientWhenAuthorizedClientNullThenIllegalArgumentException() {
OAuth2AuthorizedClient authorizedClient = null;
@@ -282,38 +246,4 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
// @formatter:on
}
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
return (actual) -> {
assertThat(actual).isNotNull();
assertThat(actual.getClientRegistration().getRegistrationId())
.isEqualTo(expected.getClientRegistration().getRegistrationId());
assertThat(actual.getClientRegistration().getClientName())
.isEqualTo(expected.getClientRegistration().getClientName());
assertThat(actual.getClientRegistration().getRedirectUri())
.isEqualTo(expected.getClientRegistration().getRedirectUri());
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
assertThat(actual.getClientRegistration().getClientId())
.isEqualTo(expected.getClientRegistration().getClientId());
assertThat(actual.getClientRegistration().getClientSecret())
.isEqualTo(expected.getClientRegistration().getClientSecret());
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
if (expected.getRefreshToken() != null) {
assertThat(actual.getRefreshToken()).isNotNull();
assertThat(actual.getRefreshToken().getTokenValue())
.isEqualTo(expected.getRefreshToken().getTokenValue());
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
assertThat(actual.getRefreshToken().getExpiresAt())
.isEqualTo(expected.getRefreshToken().getExpiresAt());
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.
@@ -57,18 +57,13 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
? resolveFromRequestParameters(request) : null;
if (authorizationHeaderToken != null) {
if (parameterToken != null) {
BearerTokenError error = BearerTokenErrors
final BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2021 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.
@@ -78,11 +78,6 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
if (!StringUtils.hasText(parameterToken)) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return parameterToken;
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2021 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.
@@ -21,11 +21,8 @@ import java.util.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -261,35 +258,4 @@ public class DefaultBearerTokenResolverTests {
assertThat(this.resolver.resolve(request)).isNull();
}
@Test
public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowUriQueryParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.addParameter("access_token", "");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
.withMessageContaining("The requested token parameter is an empty string")
.satisfies((e) -> {
BearerTokenError error = (BearerTokenError) e.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
@Test
public void resolveWhenFormParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
this.resolver.setAllowFormEncodedBodyParameter(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded");
request.addParameter("access_token", "");
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
.withMessageContaining("The requested token parameter is an empty string")
.satisfies((e) -> {
BearerTokenError error = (BearerTokenError) e.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
});
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2021 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.
@@ -187,9 +187,9 @@ public class ServerBearerTokenAuthenticationConverterTests {
.isThrownBy(() -> convertToToken(request))
.satisfies((ex) -> {
BearerTokenError error = (BearerTokenError) ex.getError();
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_TOKEN);
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.UNAUTHORIZED);
});
// @formatter:on
}
@@ -1,5 +1,5 @@
/*
* Copyright 2004-2024 the original author or authors.
* Copyright 2004-2022 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.
@@ -17,7 +17,6 @@
package org.springframework.security.taglibs.authz;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.ServletContext;
@@ -170,7 +169,7 @@ public abstract class AbstractAuthorizeTag {
}
public void setMethod(String method) {
this.method = (method != null) ? method.toUpperCase(Locale.ENGLISH) : null;
this.method = (method != null) ? method.toUpperCase() : null;
}
private SecurityContext getContext() {
@@ -16,8 +16,6 @@
package org.springframework.security.web;
import java.util.Locale;
import jakarta.servlet.ServletRequest;
import org.springframework.util.Assert;
@@ -47,7 +45,7 @@ public class PortResolverImpl implements PortResolver {
@Override
public int getServerPort(ServletRequest request) {
int serverPort = request.getServerPort();
String scheme = request.getScheme().toLowerCase(Locale.ENGLISH);
String scheme = request.getScheme().toLowerCase();
Integer mappedPort = getMappedPort(serverPort, scheme);
return (mappedPort != null) ? mappedPort : serverPort;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2023 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.
@@ -23,7 +23,6 @@ import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -98,18 +97,8 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
this.cookieCustomizer.accept(cookieBuilder);
ResponseCookie responseCookie = cookieBuilder.build();
if (!StringUtils.hasLength(responseCookie.getSameSite())) {
Cookie cookie = mapToCookie(responseCookie);
response.addCookie(cookie);
}
else if (request.getServletContext().getMajorVersion() > 5) {
Cookie cookie = mapToCookie(responseCookie);
response.addCookie(cookie);
}
else {
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
}
Cookie cookie = mapToCookie(cookieBuilder.build());
response.addCookie(cookie);
// Set request attribute to signal that response has blank cookie value,
// which allows loadToken to return null when token has been removed
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.
@@ -73,9 +73,6 @@ public class CookieRequestCache implements RequestCache {
return null;
}
String originalURI = decodeCookie(savedRequestCookie.getValue());
if (originalURI == null) {
return null;
}
UriComponents uriComponents = UriComponentsBuilder.fromUriString(originalURI).build();
DefaultSavedRequest.Builder builder = new DefaultSavedRequest.Builder();
int port = getPort(uriComponents);
@@ -125,14 +122,8 @@ public class CookieRequestCache implements RequestCache {
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
}
private String decodeCookie(String encodedCookieValue) {
try {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
catch (IllegalArgumentException ex) {
this.logger.debug("Failed decode cookie value " + encodedCookieValue);
return null;
}
private static String decodeCookie(String encodedCookieValue) {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
private static String getCookiePath(HttpServletRequest request) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -20,15 +20,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import jakarta.servlet.FilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.security.web.server.firewall.StrictServerWebExchangeFirewall;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
@@ -47,10 +43,6 @@ public class WebFilterChainProxy implements WebFilter {
private WebFilterChainDecorator filterChainDecorator = new DefaultWebFilterChainDecorator();
private ServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
private ServerExchangeRejectedHandler exchangeRejectedHandler = new HttpStatusExchangeRejectedHandler();
public WebFilterChainProxy(List<SecurityWebFilterChain> filters) {
this.filters = filters;
}
@@ -61,41 +53,14 @@ public class WebFilterChainProxy implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.firewall.getFirewalledExchange(exchange)
.flatMap((firewalledExchange) -> filterFirewalledExchange(firewalledExchange, chain))
.onErrorResume(ServerExchangeRejectedException.class,
(rejected) -> this.exchangeRejectedHandler.handle(exchange, rejected).then(Mono.empty()));
}
private Mono<Void> filterFirewalledExchange(ServerWebExchange firewalledExchange, WebFilterChain chain) {
return Flux.fromIterable(this.filters)
.filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(firewalledExchange))
.filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange))
.next()
.switchIfEmpty(Mono
.defer(() -> this.filterChainDecorator.decorate(chain).filter(firewalledExchange).then(Mono.empty())))
.switchIfEmpty(
Mono.defer(() -> this.filterChainDecorator.decorate(chain).filter(exchange).then(Mono.empty())))
.flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
.map((filters) -> this.filterChainDecorator.decorate(chain, filters))
.flatMap((securedChain) -> securedChain.filter(firewalledExchange));
}
/**
* Protects the application using the provided
* {@link StrictServerWebExchangeFirewall}.
* @param firewall the {@link StrictServerWebExchangeFirewall} to use. Cannot be null.
* @since 6.4
*/
public void setFirewall(ServerWebExchangeFirewall firewall) {
Assert.notNull(firewall, "firewall cannot be null");
this.firewall = firewall;
}
/**
* Handles {@link ServerExchangeRejectedException} when the
* {@link ServerWebExchangeFirewall} rejects the provided {@link ServerWebExchange}.
* @param exchangeRejectedHandler the {@link ServerExchangeRejectedHandler} to use.
*/
public void setExchangeRejectedHandler(ServerExchangeRejectedHandler exchangeRejectedHandler) {
this.exchangeRejectedHandler = exchangeRejectedHandler;
.flatMap((securedChain) -> securedChain.filter(exchange));
}
/**
@@ -1,66 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.server.firewall;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
/**
* A simple implementation of {@link ServerExchangeRejectedHandler} that sends an error
* with configurable status code.
*
* @author Rob Winch
* @since 6.4
*/
public class HttpStatusExchangeRejectedHandler implements ServerExchangeRejectedHandler {
private static final Log logger = LogFactory.getLog(HttpStatusExchangeRejectedHandler.class);
private final HttpStatus status;
/**
* Constructs an instance which uses {@code 400} as response code.
*/
public HttpStatusExchangeRejectedHandler() {
this(HttpStatus.BAD_REQUEST);
}
/**
* Constructs an instance which uses a configurable http code as response.
* @param status http status code to use
*/
public HttpStatusExchangeRejectedHandler(HttpStatus status) {
this.status = status;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange,
ServerExchangeRejectedException serverExchangeRejectedException) {
return Mono.fromRunnable(() -> {
logger.debug(
LogMessage.format("Rejecting request due to: %s", serverExchangeRejectedException.getMessage()),
serverExchangeRejectedException);
exchange.getResponse().setStatusCode(this.status);
});
}
}
@@ -1,31 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.server.firewall;
/**
* Thrown when a {@link org.springframework.web.server.ServerWebExchange} is rejected.
*
* @author Rob Winch
* @since 6.4
*/
public class ServerExchangeRejectedException extends RuntimeException {
public ServerExchangeRejectedException(String message) {
super(message);
}
}
@@ -1,39 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.server.firewall;
import reactor.core.publisher.Mono;
import org.springframework.web.server.ServerWebExchange;
/**
* Handles {@link ServerExchangeRejectedException} thrown by
* {@link ServerWebExchangeFirewall}.
*
* @author Rob Winch
* @since 6.4
*/
public interface ServerExchangeRejectedHandler {
/**
* Handles an request rejected failure.
* @param exchange the {@link ServerWebExchange} that was rejected
* @param serverExchangeRejectedException that caused the invocation
*/
Mono<Void> handle(ServerWebExchange exchange, ServerExchangeRejectedException serverExchangeRejectedException);
}
@@ -1,46 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.server.firewall;
import reactor.core.publisher.Mono;
import org.springframework.web.server.ServerWebExchange;
/**
* Interface which can be used to reject potentially dangerous requests and/or wrap them
* to control their behaviour.
*
* @author Rob Winch
* @since 6.4
*/
public interface ServerWebExchangeFirewall {
/**
* An implementation of {@link StrictServerWebExchangeFirewall} that does nothing.
* This is considered insecure and not recommended.
*/
ServerWebExchangeFirewall INSECURE_NOOP = (exchange) -> Mono.just(exchange);
/**
* Get a {@link ServerWebExchange} that has firewall rules applied to it.
* @param exchange the {@link ServerWebExchange} to apply firewall rules to.
* @return the {@link ServerWebExchange} that has firewall rules applied to it.
* @throws ServerExchangeRejectedException when a rule is broken.
*/
Mono<ServerWebExchange> getFirewalledExchange(ServerWebExchange exchange);
}
@@ -1,790 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.server.firewall;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
/**
* <p>
* A strict implementation of {@link ServerWebExchangeFirewall} that rejects any
* suspicious requests with a {@link ServerExchangeRejectedException}.
* </p>
* <p>
* The following rules are applied to the firewall:
* </p>
* <ul>
* <li>Rejects HTTP methods that are not allowed. This specified to block
* <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)">HTTP Verb
* tampering and XST attacks</a>. See {@link #setAllowedHttpMethods(Collection)}</li>
* <li>Rejects URLs that are not normalized to avoid bypassing security constraints. There
* is no way to disable this as it is considered extremely risky to disable this
* constraint. A few options to allow this behavior is to normalize the request prior to
* the firewall or using
* {@link org.springframework.security.web.firewall.DefaultHttpFirewall} instead. Please
* keep in mind that normalizing the request is fragile and why requests are rejected
* rather than normalized.</li>
* <li>Rejects URLs that contain characters that are not printable ASCII characters. There
* is no way to disable this as it is considered extremely risky to disable this
* constraint.</li>
* <li>Rejects URLs that contain semicolons. See {@link #setAllowSemicolon(boolean)}</li>
* <li>Rejects URLs that contain a URL encoded slash. See
* {@link #setAllowUrlEncodedSlash(boolean)}</li>
* <li>Rejects URLs that contain a backslash. See {@link #setAllowBackSlash(boolean)}</li>
* <li>Rejects URLs that contain a null character. See {@link #setAllowNull(boolean)}</li>
* <li>Rejects URLs that contain a URL encoded percent. See
* {@link #setAllowUrlEncodedPercent(boolean)}</li>
* <li>Rejects hosts that are not allowed. See {@link #setAllowedHostnames(Predicate)}
* </li>
* <li>Reject headers names that are not allowed. See
* {@link #setAllowedHeaderNames(Predicate)}</li>
* <li>Reject headers values that are not allowed. See
* {@link #setAllowedHeaderValues(Predicate)}</li>
* <li>Reject parameter names that are not allowed. See
* {@link #setAllowedParameterNames(Predicate)}</li>
* <li>Reject parameter values that are not allowed. See
* {@link #setAllowedParameterValues(Predicate)}</li>
* </ul>
*
* @author Rob Winch
* @since 6.4
*/
public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewall {
/**
* Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method
* should be allowed.
*/
private static final Set<HttpMethod> ALLOW_ANY_HTTP_METHOD = Collections.emptySet();
private static final String ENCODED_PERCENT = "%25";
private static final String PERCENT = "%";
private static final List<String> FORBIDDEN_ENCODED_PERIOD = Collections
.unmodifiableList(Arrays.asList("%2e", "%2E"));
private static final List<String> FORBIDDEN_SEMICOLON = Collections
.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
private static final List<String> FORBIDDEN_FORWARDSLASH = Collections
.unmodifiableList(Arrays.asList("%2f", "%2F"));
private static final List<String> FORBIDDEN_DOUBLE_FORWARDSLASH = Collections
.unmodifiableList(Arrays.asList("//", "%2f%2f", "%2f%2F", "%2F%2f", "%2F%2F"));
private static final List<String> FORBIDDEN_BACKSLASH = Collections
.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
private static final List<String> FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00"));
private static final List<String> FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A"));
private static final List<String> FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\r", "%0d", "%0D"));
private static final List<String> FORBIDDEN_LINE_SEPARATOR = Collections.unmodifiableList(Arrays.asList("\u2028"));
private static final List<String> FORBIDDEN_PARAGRAPH_SEPARATOR = Collections
.unmodifiableList(Arrays.asList("\u2029"));
private Set<String> encodedUrlBlocklist = new HashSet<>();
private Set<String> decodedUrlBlocklist = new HashSet<>();
private Set<HttpMethod> allowedHttpMethods = createDefaultAllowedHttpMethods();
private Predicate<String> allowedHostnames = (hostname) -> true;
private static final Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern
.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> s == null
|| HEADER_VALUE_PATTERN.matcher(s).matches();
private Predicate<String> allowedHeaderNames = ALLOWED_HEADER_NAMES;
public static final Predicate<String> ALLOWED_HEADER_NAMES = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
private Predicate<String> allowedHeaderValues = ALLOWED_HEADER_VALUES;
public static final Predicate<String> ALLOWED_HEADER_VALUES = HEADER_VALUE_PREDICATE;
private Predicate<String> allowedParameterNames = ALLOWED_PARAMETER_NAMES;
public static final Predicate<String> ALLOWED_PARAMETER_NAMES = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
private Predicate<String> allowedParameterValues = ALLOWED_PARAMETER_VALUES;
public static final Predicate<String> ALLOWED_PARAMETER_VALUES = (value) -> true;
public StrictServerWebExchangeFirewall() {
urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
urlBlocklistsAddAll(FORBIDDEN_BACKSLASH);
urlBlocklistsAddAll(FORBIDDEN_NULL);
urlBlocklistsAddAll(FORBIDDEN_LF);
urlBlocklistsAddAll(FORBIDDEN_CR);
this.encodedUrlBlocklist.add(ENCODED_PERCENT);
this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
this.decodedUrlBlocklist.add(PERCENT);
this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR);
this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
}
public Set<String> getEncodedUrlBlocklist() {
return this.encodedUrlBlocklist;
}
public Set<String> getDecodedUrlBlocklist() {
return this.decodedUrlBlocklist;
}
@Override
public Mono<ServerWebExchange> getFirewalledExchange(ServerWebExchange exchange) {
return Mono.fromCallable(() -> {
ServerHttpRequest request = exchange.getRequest();
rejectForbiddenHttpMethod(request);
rejectedBlocklistedUrls(request);
rejectedUntrustedHosts(request);
if (!isNormalized(request)) {
throw new ServerExchangeRejectedException(
"The request was rejected because the URL was not normalized");
}
exchange.getResponse().beforeCommit(() -> Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
String headerName = header.getKey();
List<String> headerValues = header.getValue();
for (String headerValue : headerValues) {
validateCrlf(headerName, headerValue);
}
}
}));
return new StrictFirewallServerWebExchange(exchange);
});
}
private static void validateCrlf(String name, String value) {
Assert.isTrue(!hasCrlf(name) && !hasCrlf(value), () -> "Invalid characters (CR/LF) in header " + name);
}
private static boolean hasCrlf(String value) {
return value != null && (value.indexOf('\n') != -1 || value.indexOf('\r') != -1);
}
/**
* Sets if any HTTP method is allowed. If this set to true, then no validation on the
* HTTP method will be performed. This can open the application up to
* <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)"> HTTP
* Verb tampering and XST attacks</a>
* @param unsafeAllowAnyHttpMethod if true, disables HTTP method validation, else
* resets back to the defaults. Default is false.
* @since 5.1
* @see #setAllowedHttpMethods(Collection)
*/
public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
}
/**
* <p>
* Determines which HTTP methods should be allowed. The default is to allow "DELETE",
* "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT".
* </p>
* @param allowedHttpMethods the case-sensitive collection of HTTP methods that are
* allowed.
* @since 5.1
* @see #setUnsafeAllowAnyHttpMethod(boolean)
*/
public void setAllowedHttpMethods(Collection<HttpMethod> allowedHttpMethods) {
Assert.notNull(allowedHttpMethods, "allowedHttpMethods cannot be null");
this.allowedHttpMethods = (allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) ? new HashSet<>(allowedHttpMethods)
: ALLOW_ANY_HTTP_METHOD;
}
/**
* <p>
* Determines if semicolon is allowed in the URL (i.e. matrix variables). The default
* is to disable this behavior because it is a common way of attempting to perform
* <a href="https://www.owasp.org/index.php/Reflected_File_Download">Reflected File
* Download Attacks</a>. It is also the source of many exploits which bypass URL based
* security.
* </p>
* <p>
* For example, the following CVEs are a subset of the issues related to ambiguities
* in the Servlet Specification on how to treat semicolons that led to CVEs:
* </p>
* <ul>
* <li><a href="https://pivotal.io/security/cve-2016-5007">cve-2016-5007</a></li>
* <li><a href="https://pivotal.io/security/cve-2016-9879">cve-2016-9879</a></li>
* <li><a href="https://pivotal.io/security/cve-2018-1199">cve-2018-1199</a></li>
* </ul>
*
* <p>
* If you are wanting to allow semicolons, please reconsider as it is a very common
* source of security bypasses. A few common reasons users want semicolons and
* alternatives are listed below:
* </p>
* <ul>
* <li>Including the JSESSIONID in the path - You should not include session id (or
* any sensitive information) in a URL as it can lead to leaking. Instead use Cookies.
* </li>
* <li>Matrix Variables - Users wanting to leverage Matrix Variables should consider
* using HTTP parameters instead.</li>
* </ul>
* @param allowSemicolon should semicolons be allowed in the URL. Default is false
*/
public void setAllowSemicolon(boolean allowSemicolon) {
if (allowSemicolon) {
urlBlocklistsRemoveAll(FORBIDDEN_SEMICOLON);
}
else {
urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
}
}
/**
* <p>
* Determines if a slash "/" that is URL encoded "%2F" should be allowed in the path
* or not. The default is to not allow this behavior because it is a common way to
* bypass URL based security.
* </p>
* <p>
* For example, due to ambiguities in the servlet specification, the value is not
* parsed consistently which results in different values in {@code HttpServletRequest}
* path related values which allow bypassing certain security constraints.
* </p>
* @param allowUrlEncodedSlash should a slash "/" that is URL encoded "%2F" be allowed
* in the path or not. Default is false.
*/
public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
if (allowUrlEncodedSlash) {
urlBlocklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
}
else {
urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
}
}
/**
* <p>
* Determines if double slash "//" that is URL encoded "%2F%2F" should be allowed in
* the path or not. The default is to not allow.
* </p>
* @param allowUrlEncodedDoubleSlash should a slash "//" that is URL encoded "%2F%2F"
* be allowed in the path or not. Default is false.
*/
public void setAllowUrlEncodedDoubleSlash(boolean allowUrlEncodedDoubleSlash) {
if (allowUrlEncodedDoubleSlash) {
urlBlocklistsRemoveAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
}
else {
urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
}
}
/**
* <p>
* Determines if a period "." that is URL encoded "%2E" should be allowed in the path
* or not. The default is to not allow this behavior because it is a frequent source
* of security exploits.
* </p>
* <p>
* For example, due to ambiguities in the servlet specification a URL encoded period
* might lead to bypassing security constraints through a directory traversal attack.
* This is because the path is not parsed consistently which results in different
* values in {@code HttpServletRequest} path related values which allow bypassing
* certain security constraints.
* </p>
* @param allowUrlEncodedPeriod should a period "." that is URL encoded "%2E" be
* allowed in the path or not. Default is false.
*/
public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) {
if (allowUrlEncodedPeriod) {
this.encodedUrlBlocklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
}
else {
this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
}
}
/**
* <p>
* Determines if a backslash "\" or a URL encoded backslash "%5C" should be allowed in
* the path or not. The default is not to allow this behavior because it is a frequent
* source of security exploits.
* </p>
* <p>
* For example, due to ambiguities in the servlet specification a URL encoded period
* might lead to bypassing security constraints through a directory traversal attack.
* This is because the path is not parsed consistently which results in different
* values in {@code HttpServletRequest} path related values which allow bypassing
* certain security constraints.
* </p>
* @param allowBackSlash a backslash "\" or a URL encoded backslash "%5C" be allowed
* in the path or not. Default is false
*/
public void setAllowBackSlash(boolean allowBackSlash) {
if (allowBackSlash) {
urlBlocklistsRemoveAll(FORBIDDEN_BACKSLASH);
}
else {
urlBlocklistsAddAll(FORBIDDEN_BACKSLASH);
}
}
/**
* <p>
* Determines if a null "\0" or a URL encoded nul "%00" should be allowed in the path
* or not. The default is not to allow this behavior because it is a frequent source
* of security exploits.
* </p>
* @param allowNull a null "\0" or a URL encoded null "%00" be allowed in the path or
* not. Default is false
* @since 5.4
*/
public void setAllowNull(boolean allowNull) {
if (allowNull) {
urlBlocklistsRemoveAll(FORBIDDEN_NULL);
}
else {
urlBlocklistsAddAll(FORBIDDEN_NULL);
}
}
/**
* <p>
* Determines if a percent "%" that is URL encoded "%25" should be allowed in the path
* or not. The default is not to allow this behavior because it is a frequent source
* of security exploits.
* </p>
* <p>
* For example, this can lead to exploits that involve double URL encoding that lead
* to bypassing security constraints.
* </p>
* @param allowUrlEncodedPercent if a percent "%" that is URL encoded "%25" should be
* allowed in the path or not. Default is false
*/
public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
if (allowUrlEncodedPercent) {
this.encodedUrlBlocklist.remove(ENCODED_PERCENT);
this.decodedUrlBlocklist.remove(PERCENT);
}
else {
this.encodedUrlBlocklist.add(ENCODED_PERCENT);
this.decodedUrlBlocklist.add(PERCENT);
}
}
/**
* Determines if a URL encoded Carriage Return is allowed in the path or not. The
* default is not to allow this behavior because it is a frequent source of security
* exploits.
* @param allowUrlEncodedCarriageReturn if URL encoded Carriage Return is allowed in
* the URL or not. Default is false.
*/
public void setAllowUrlEncodedCarriageReturn(boolean allowUrlEncodedCarriageReturn) {
if (allowUrlEncodedCarriageReturn) {
urlBlocklistsRemoveAll(FORBIDDEN_CR);
}
else {
urlBlocklistsAddAll(FORBIDDEN_CR);
}
}
/**
* Determines if a URL encoded Line Feed is allowed in the path or not. The default is
* not to allow this behavior because it is a frequent source of security exploits.
* @param allowUrlEncodedLineFeed if URL encoded Line Feed is allowed in the URL or
* not. Default is false.
*/
public void setAllowUrlEncodedLineFeed(boolean allowUrlEncodedLineFeed) {
if (allowUrlEncodedLineFeed) {
urlBlocklistsRemoveAll(FORBIDDEN_LF);
}
else {
urlBlocklistsAddAll(FORBIDDEN_LF);
}
}
/**
* Determines if a URL encoded paragraph separator is allowed in the path or not. The
* default is not to allow this behavior because it is a frequent source of security
* exploits.
* @param allowUrlEncodedParagraphSeparator if URL encoded paragraph separator is
* allowed in the URL or not. Default is false.
*/
public void setAllowUrlEncodedParagraphSeparator(boolean allowUrlEncodedParagraphSeparator) {
if (allowUrlEncodedParagraphSeparator) {
this.decodedUrlBlocklist.removeAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
}
else {
this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
}
}
/**
* Determines if a URL encoded line separator is allowed in the path or not. The
* default is not to allow this behavior because it is a frequent source of security
* exploits.
* @param allowUrlEncodedLineSeparator if URL encoded line separator is allowed in the
* URL or not. Default is false.
*/
public void setAllowUrlEncodedLineSeparator(boolean allowUrlEncodedLineSeparator) {
if (allowUrlEncodedLineSeparator) {
this.decodedUrlBlocklist.removeAll(FORBIDDEN_LINE_SEPARATOR);
}
else {
this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR);
}
}
/**
* <p>
* Determines which header names should be allowed. The default is to reject header
* names that contain ISO control characters and characters that are not defined.
* </p>
* @param allowedHeaderNames the predicate for testing header names
* @since 5.4
* @see Character#isISOControl(int)
* @see Character#isDefined(int)
*/
public void setAllowedHeaderNames(Predicate<String> allowedHeaderNames) {
Assert.notNull(allowedHeaderNames, "allowedHeaderNames cannot be null");
this.allowedHeaderNames = allowedHeaderNames;
}
/**
* <p>
* Determines which header values should be allowed. The default is to reject header
* values that contain ISO control characters and characters that are not defined.
* </p>
* @param allowedHeaderValues the predicate for testing hostnames
* @since 5.4
* @see Character#isISOControl(int)
* @see Character#isDefined(int)
*/
public void setAllowedHeaderValues(Predicate<String> allowedHeaderValues) {
Assert.notNull(allowedHeaderValues, "allowedHeaderValues cannot be null");
this.allowedHeaderValues = allowedHeaderValues;
}
/**
* Determines which parameter names should be allowed. The default is to reject header
* names that contain ISO control characters and characters that are not defined.
* @param allowedParameterNames the predicate for testing parameter names
* @since 5.4
* @see Character#isISOControl(int)
* @see Character#isDefined(int)
*/
public void setAllowedParameterNames(Predicate<String> allowedParameterNames) {
Assert.notNull(allowedParameterNames, "allowedParameterNames cannot be null");
this.allowedParameterNames = allowedParameterNames;
}
/**
* <p>
* Determines which parameter values should be allowed. The default is to allow any
* parameter value.
* </p>
* @param allowedParameterValues the predicate for testing parameter values
* @since 5.4
*/
public void setAllowedParameterValues(Predicate<String> allowedParameterValues) {
Assert.notNull(allowedParameterValues, "allowedParameterValues cannot be null");
this.allowedParameterValues = allowedParameterValues;
}
/**
* <p>
* Determines which hostnames should be allowed. The default is to allow any hostname.
* </p>
* @param allowedHostnames the predicate for testing hostnames
* @since 5.2
*/
public void setAllowedHostnames(Predicate<String> allowedHostnames) {
Assert.notNull(allowedHostnames, "allowedHostnames cannot be null");
this.allowedHostnames = allowedHostnames;
}
private void urlBlocklistsAddAll(Collection<String> values) {
this.encodedUrlBlocklist.addAll(values);
this.decodedUrlBlocklist.addAll(values);
}
private void urlBlocklistsRemoveAll(Collection<String> values) {
this.encodedUrlBlocklist.removeAll(values);
this.decodedUrlBlocklist.removeAll(values);
}
private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) {
if (!containsOnlyPrintableAsciiCharacters(toCheck)) {
throw new ServerExchangeRejectedException(String
.format("The %s was rejected because it can only contain printable ASCII characters.", propertyName));
}
}
private void rejectForbiddenHttpMethod(ServerHttpRequest request) {
if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
return;
}
if (!this.allowedHttpMethods.contains(request.getMethod())) {
throw new ServerExchangeRejectedException(
"The request was rejected because the HTTP method \"" + request.getMethod()
+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
}
}
private void rejectedBlocklistedUrls(ServerHttpRequest request) {
for (String forbidden : this.encodedUrlBlocklist) {
if (encodedUrlContains(request, forbidden)) {
throw new ServerExchangeRejectedException(
"The request was rejected because the URL contained a potentially malicious String \""
+ forbidden + "\"");
}
}
for (String forbidden : this.decodedUrlBlocklist) {
if (decodedUrlContains(request, forbidden)) {
throw new ServerExchangeRejectedException(
"The request was rejected because the URL contained a potentially malicious String \""
+ forbidden + "\"");
}
}
}
private void rejectedUntrustedHosts(ServerHttpRequest request) {
String hostName = request.getURI().getHost();
if (hostName != null && !this.allowedHostnames.test(hostName)) {
throw new ServerExchangeRejectedException(
"The request was rejected because the domain " + hostName + " is untrusted.");
}
}
private static Set<HttpMethod> createDefaultAllowedHttpMethods() {
Set<HttpMethod> result = new HashSet<>();
result.add(HttpMethod.DELETE);
result.add(HttpMethod.GET);
result.add(HttpMethod.HEAD);
result.add(HttpMethod.OPTIONS);
result.add(HttpMethod.PATCH);
result.add(HttpMethod.POST);
result.add(HttpMethod.PUT);
return result;
}
private boolean isNormalized(ServerHttpRequest request) {
if (!isNormalized(request.getPath().value())) {
return false;
}
if (!isNormalized(request.getURI().getRawPath())) {
return false;
}
if (!isNormalized(request.getURI().getPath())) {
return false;
}
return true;
}
private void validateAllowedHeaderName(String headerNames) {
if (!StrictServerWebExchangeFirewall.this.allowedHeaderNames.test(headerNames)) {
throw new ServerExchangeRejectedException(
"The request was rejected because the header name \"" + headerNames + "\" is not allowed.");
}
}
private void validateAllowedHeaderValue(Object key, String value) {
if (!StrictServerWebExchangeFirewall.this.allowedHeaderValues.test(value)) {
throw new ServerExchangeRejectedException("The request was rejected because the header: \"" + key
+ " \" has a value \"" + value + "\" that is not allowed.");
}
}
private void validateAllowedParameterName(String name) {
if (!StrictServerWebExchangeFirewall.this.allowedParameterNames.test(name)) {
throw new ServerExchangeRejectedException(
"The request was rejected because the parameter name \"" + name + "\" is not allowed.");
}
}
private void validateAllowedParameterValue(String name, String value) {
if (!StrictServerWebExchangeFirewall.this.allowedParameterValues.test(value)) {
throw new ServerExchangeRejectedException("The request was rejected because the parameter: \"" + name
+ " \" has a value \"" + value + "\" that is not allowed.");
}
}
private static boolean encodedUrlContains(ServerHttpRequest request, String value) {
if (valueContains(request.getPath().value(), value)) {
return true;
}
return valueContains(request.getURI().getRawPath(), value);
}
private static boolean decodedUrlContains(ServerHttpRequest request, String value) {
return valueContains(request.getURI().getPath(), value);
}
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
if (uri == null) {
return true;
}
int length = uri.length();
for (int i = 0; i < length; i++) {
char ch = uri.charAt(i);
if (ch < '\u0020' || ch > '\u007e') {
return false;
}
}
return true;
}
private static boolean valueContains(String value, String contains) {
return value != null && value.contains(contains);
}
/**
* Checks whether a path is normalized (doesn't contain path traversal sequences like
* "./", "/../" or "/.")
* @param path the path to test
* @return true if the path doesn't contain any path-traversal character sequences.
*/
private static boolean isNormalized(String path) {
if (path == null) {
return true;
}
for (int i = path.length(); i > 0;) {
int slashIndex = path.lastIndexOf('/', i - 1);
int gap = i - slashIndex;
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
return false; // ".", "/./" or "/."
}
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
return false;
}
i = slashIndex;
}
return true;
}
private final class StrictFirewallServerWebExchange extends ServerWebExchangeDecorator {
private StrictFirewallServerWebExchange(ServerWebExchange delegate) {
super(delegate);
}
@Override
public ServerHttpRequest getRequest() {
return new StrictFirewallHttpRequest(super.getRequest());
}
private final class StrictFirewallHttpRequest extends ServerHttpRequestDecorator {
private StrictFirewallHttpRequest(ServerHttpRequest delegate) {
super(delegate);
}
@Override
public HttpHeaders getHeaders() {
return new StrictFirewallHttpHeaders(super.getHeaders());
}
@Override
public MultiValueMap<String, String> getQueryParams() {
MultiValueMap<String, String> queryParams = super.getQueryParams();
for (Map.Entry<String, List<String>> paramEntry : queryParams.entrySet()) {
String paramName = paramEntry.getKey();
validateAllowedParameterName(paramName);
for (String paramValue : paramEntry.getValue()) {
validateAllowedParameterValue(paramName, paramValue);
}
}
return queryParams;
}
private final class StrictFirewallHttpHeaders extends HttpHeaders {
private StrictFirewallHttpHeaders(HttpHeaders delegate) {
super(delegate);
}
@Override
public String getFirst(String headerName) {
validateAllowedHeaderName(headerName);
String headerValue = super.getFirst(headerName);
validateAllowedHeaderValue(headerName, headerValue);
return headerValue;
}
@Override
public List<String> get(Object key) {
if (key instanceof String headerName) {
validateAllowedHeaderName(headerName);
}
List<String> headerValues = super.get(key);
if (headerValues == null) {
return headerValues;
}
for (String headerValue : headerValues) {
validateAllowedHeaderValue(key, headerValue);
}
return headerValues;
}
@Override
public Set<String> keySet() {
Set<String> headerNames = super.keySet();
for (String headerName : headerNames) {
validateAllowedHeaderName(headerName);
}
return headerNames;
}
}
}
}
}
@@ -16,7 +16,6 @@
package org.springframework.security.web.util;
import java.util.Locale;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest;
@@ -50,7 +49,7 @@ public final class UrlUtils {
*/
public static String buildFullRequestUrl(String scheme, String serverName, int serverPort, String requestURI,
String queryString) {
scheme = scheme.toLowerCase(Locale.ENGLISH);
scheme = scheme.toLowerCase();
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);
// Only add port if not default
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 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.
@@ -17,7 +17,6 @@
package org.springframework.security.web.util.matcher;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
@@ -304,7 +303,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
private SubpathMatcher(String subpath, boolean caseSensitive) {
Assert.isTrue(!subpath.contains("*"), "subpath cannot contain \"*\"");
this.subpath = caseSensitive ? subpath : subpath.toLowerCase(Locale.ROOT);
this.subpath = caseSensitive ? subpath : subpath.toLowerCase();
this.length = subpath.length();
this.caseSensitive = caseSensitive;
}
@@ -312,7 +311,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
@Override
public boolean matches(String path) {
if (!this.caseSensitive) {
path = path.toLowerCase(Locale.ROOT);
path = path.toLowerCase();
}
return path.startsWith(this.subpath) && (path.length() == this.length || path.charAt(this.length) == '/');
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.
@@ -17,20 +17,16 @@
package org.springframework.security.web.csrf;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
@@ -451,44 +447,6 @@ class CookieCsrfTokenRepositoryTests {
assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.FALSE);
}
// gh-16173
@Test
void saveTokenWhenSameSiteAndServletVersion5ThenUsesAddHeader() {
HttpServletResponse response = mock(HttpServletResponse.class);
((MockServletContext) this.request.getServletContext()).setMajorVersion(5);
this.repository.setCookieCustomizer((builder) -> builder.sameSite("Strict"));
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, response);
verify(response, never()).addCookie(any(Cookie.class));
verify(response).addHeader(any(), any());
}
// gh-16173
@Test
void saveTokenWhenSameSiteAndServletVersion6OrHigherThenUsesAddCookie() {
HttpServletResponse response = mock(HttpServletResponse.class);
this.repository.setCookieCustomizer((builder) -> builder.sameSite("Strict"));
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, response);
verify(response).addCookie(any(Cookie.class));
verify(response, never()).addHeader(any(), any());
}
// gh-16173
@Test
void saveTokenWhenNoSameSiteThenUsesAddCookie() {
HttpServletResponse response = mock(HttpServletResponse.class);
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, response);
verify(response).addCookie(any(Cookie.class));
verify(response, never()).addHeader(any(), any());
((MockServletContext) this.request.getServletContext()).setMajorVersion(5);
response = mock(HttpServletResponse.class);
this.repository.saveToken(token, this.request, response);
verify(response).addCookie(any(Cookie.class));
verify(response, never()).addHeader(any(), any());
}
@Test
void setCookieNameNullIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 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.
@@ -212,14 +212,4 @@ public class CookieRequestCacheTests {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
// gh-15905
@Test
public void illegalCookieValueReturnNull() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, "123^456"));
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
assertThat(savedRequest).isNull();
}
}
@@ -27,16 +27,12 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.web.server.ObservationWebFilterChainDecorator.WebFilterChainObservationContext;
import org.springframework.security.web.server.ObservationWebFilterChainDecorator.WebFilterChainObservationConvention;
import org.springframework.security.web.server.ObservationWebFilterChainDecorator.WebFilterObservation;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -52,7 +48,6 @@ import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* @author Rob Winch
@@ -148,77 +143,6 @@ public class WebFilterChainProxyTests {
assertFilterChainObservation(contexts.next(), "before", 1);
}
@Test
void doFilterWhenFirewallThenBadRequest() {
List<WebFilter> filters = Arrays.asList(new Http200WebFilter());
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
WebTestClient.bindToController(new Object())
.webFilter(filter)
.build()
.method(HttpMethod.valueOf("INVALID"))
.exchange()
.expectStatus()
.isBadRequest();
}
@Test
void doFilterWhenCustomFirewallThenInvoked() {
List<WebFilter> filters = Arrays.asList(new Http200WebFilter());
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
ServerExchangeRejectedHandler handler = mock(ServerExchangeRejectedHandler.class);
ServerWebExchangeFirewall firewall = mock(ServerWebExchangeFirewall.class);
filter.setFirewall(firewall);
filter.setExchangeRejectedHandler(handler);
WebTestClient.bindToController(new Object()).webFilter(filter).build().get().exchange();
verify(firewall).getFirewalledExchange(any());
verifyNoInteractions(handler);
}
@Test
void doFilterWhenCustomExchangeRejectedHandlerThenInvoked() {
List<WebFilter> filters = Arrays.asList(new Http200WebFilter());
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
ServerExchangeRejectedHandler handler = mock(ServerExchangeRejectedHandler.class);
ServerWebExchangeFirewall firewall = mock(ServerWebExchangeFirewall.class);
given(firewall.getFirewalledExchange(any()))
.willReturn(Mono.error(new ServerExchangeRejectedException("Oops")));
filter.setFirewall(firewall);
filter.setExchangeRejectedHandler(handler);
WebTestClient.bindToController(new Object()).webFilter(filter).build().get().exchange();
verify(firewall).getFirewalledExchange(any());
verify(handler).handle(any(), any());
}
@Test
void doFilterWhenDelayedServerExchangeRejectedException() {
List<WebFilter> filters = Arrays.asList(new WebFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// simulate a delayed error (e.g. reading parameters)
return Mono.error(new ServerExchangeRejectedException("Ooops"));
}
});
ServerWebExchangeMatcher match = (exchange) -> MatchResult.match();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(match, filters);
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
ServerExchangeRejectedHandler handler = mock(ServerExchangeRejectedHandler.class);
filter.setExchangeRejectedHandler(handler);
// @formatter:off
WebTestClient.bindToController(new Object())
.webFilter(filter)
.build()
.get()
.exchange();
// @formatter:on
verify(handler).handle(any(), any());
}
static void assertFilterChainObservation(Observation.Context context, String filterSection, int chainPosition) {
assertThat(context).isInstanceOf(WebFilterChainObservationContext.class);
WebFilterChainObservationContext filterChainObservationContext = (WebFilterChainObservationContext) context;
@@ -1,516 +0,0 @@
/*
* Copyright 2002-2024 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
*
* https://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.server.firewall;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link StrictServerWebExchangeFirewall}.
*
* @author Rob Winch
* @since 6.4
*/
class StrictServerWebExchangeFirewallTests {
public String[] unnormalizedPaths = { "http://exploit.example/..", "http://exploit.example/./path/",
"http://exploit.example/path/path/.", "http://exploit.example/path/path//.",
"http://exploit.example/./path/../path//.", "http://exploit.example/./path",
"http://exploit.example/.//path", "http://exploit.example/.", "http://exploit.example//path",
"http://exploit.example//path/path", "http://exploit.example//path//path",
"http://exploit.example/path//path" };
private StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
private MockServerHttpRequest.BaseBuilder<?> request = get("/");
@Test
void cookieWhenHasNewLineThenThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResponseCookie.from("test").value("Something\nhere").build());
}
@Test
void cookieWhenHasLineFeedThenThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResponseCookie.from("test").value("Something\rhere").build());
}
@Test
void responseHeadersWhenValueHasNewLineThenThrowsException() {
this.request = MockServerHttpRequest.get("/");
ServerWebExchange exchange = getFirewalledExchange();
exchange.getResponse().getHeaders().set("FOO", "new\nline");
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
}
@Test
void responseHeadersWhenValueHasLineFeedThenThrowsException() {
this.request = MockServerHttpRequest.get("/");
ServerWebExchange exchange = getFirewalledExchange();
exchange.getResponse().getHeaders().set("FOO", "line\rfeed");
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
}
@Test
void responseHeadersWhenNameHasNewLineThenThrowsException() {
this.request = MockServerHttpRequest.get("/");
ServerWebExchange exchange = getFirewalledExchange();
exchange.getResponse().getHeaders().set("new\nline", "FOO");
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
}
@Test
void responseHeadersWhenNameHasLineFeedThenThrowsException() {
this.request = MockServerHttpRequest.get("/");
ServerWebExchange exchange = getFirewalledExchange();
exchange.getResponse().getHeaders().set("line\rfeed", "FOO");
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
}
@Test
void getFirewalledExchangeWhenInvalidMethodThenThrowsServerExchangeRejectedException() {
this.request = MockServerHttpRequest.method(HttpMethod.valueOf("INVALID"), "/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
private ServerWebExchange getFirewalledExchange() {
MockServerWebExchange exchange = MockServerWebExchange.from(this.request.build());
return this.firewall.getFirewalledExchange(exchange).block();
}
private MockServerHttpRequest.BodyBuilder get(String uri) {
URI url = URI.create(uri);
return MockServerHttpRequest.method(HttpMethod.GET, url);
}
// blocks XST attacks
@Test
void getFirewalledExchangeWhenTraceMethodThenThrowsServerExchangeRejectedException() {
this.request = MockServerHttpRequest.method(HttpMethod.TRACE, "/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
// blocks XST attack if request is forwarded to a Microsoft IIS web server
void getFirewalledExchangeWhenTrackMethodThenThrowsServerExchangeRejectedException() {
this.request = MockServerHttpRequest.method(HttpMethod.TRACE, "/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
// HTTP methods are case sensitive
void getFirewalledExchangeWhenLowercaseGetThenThrowsServerExchangeRejectedException() {
this.request = MockServerHttpRequest.method(HttpMethod.valueOf("get"), "/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenAllowedThenNoException() {
List<String> allowedMethods = Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT");
for (String allowedMethod : allowedMethods) {
this.request = MockServerHttpRequest.method(HttpMethod.valueOf(allowedMethod), "/");
getFirewalledExchange();
}
}
@Test
void getFirewalledExchangeWhenInvalidMethodAndAnyMethodThenNoException() {
this.firewall.setUnsafeAllowAnyHttpMethod(true);
this.request = MockServerHttpRequest.method(HttpMethod.valueOf("INVALID"), "/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenURINotNormalizedThenThrowsServerExchangeRejectedException() {
for (String path : this.unnormalizedPaths) {
this.request = get(path);
assertThatExceptionOfType(ServerExchangeRejectedException.class)
.describedAs("The path '" + path + "' is not normalized")
.isThrownBy(() -> getFirewalledExchange());
}
}
@Test
void getFirewalledExchangeWhenSemicolonInRequestUriThenThrowsServerExchangeRejectedException() {
this.request = get("/path;/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenEncodedSemicolonInRequestUriThenThrowsServerExchangeRejectedException() {
this.request = get("/path%3B/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenLowercaseEncodedSemicolonInRequestUriThenThrowsServerExchangeRejectedException() {
this.request = MockServerHttpRequest.method(HttpMethod.GET, URI.create("/path%3b/"));
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenSemicolonInRequestUriAndAllowSemicolonThenNoException() {
this.firewall.setAllowSemicolon(true);
this.request = get("/path;/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() {
this.firewall.setAllowSemicolon(true);
this.request = get("/path%3B/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenLowercaseEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() {
this.firewall.setAllowSemicolon(true);
this.request = get("/path%3b/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenLowercaseEncodedPeriodInThenThrowsServerExchangeRejectedException() {
this.request = get("/%2e/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenContainsLowerboundAsciiThenNoException() {
this.request = get("/%20");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenContainsUpperboundAsciiThenNoException() {
this.request = get("/~");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenJapaneseCharacterThenNoException() {
// FIXME: .method(HttpMethod.GET to .get and similar methods
this.request = get("/\u3042");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenContainsEncodedNullThenException() {
this.request = get("/something%00/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenContainsLowercaseEncodedLineFeedThenException() {
this.request = get("/something%0a/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenContainsUppercaseEncodedLineFeedThenException() {
this.request = get("/something%0A/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenContainsLowercaseEncodedCarriageReturnThenException() {
this.request = get("/something%0d/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenContainsUppercaseEncodedCarriageReturnThenException() {
this.request = get("/something%0D/");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenContainsLowercaseEncodedLineFeedAndAllowedThenNoException() {
this.firewall.setAllowUrlEncodedLineFeed(true);
this.request = get("/something%0a/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenContainsUppercaseEncodedLineFeedAndAllowedThenNoException() {
this.firewall.setAllowUrlEncodedLineFeed(true);
this.request = get("/something%0A/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenContainsLowercaseEncodedCarriageReturnAndAllowedThenNoException() {
this.firewall.setAllowUrlEncodedCarriageReturn(true);
this.request = get("/something%0d/");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenContainsUppercaseEncodedCarriageReturnAndAllowedThenNoException() {
this.firewall.setAllowUrlEncodedCarriageReturn(true);
this.request = get("/something%0D/");
getFirewalledExchange();
}
/**
* On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on /a/b/c
* because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c while Spring MVC
* will strip the ; content from requestURI before the path is URL decoded.
*/
@Test
void getFirewalledExchangeWhenLowercaseEncodedPathThenException() {
this.request = get("/context-root/a/b;%2f1/c");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenUppercaseEncodedPathThenException() {
this.request = get("/context-root/a/b;%2F1/c");
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeWhenAllowUrlEncodedSlashAndLowercaseEncodedPathThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowSemicolon(true);
this.request = get("/context-root/a/b;%2f1/c");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenAllowUrlEncodedSlashAndUppercaseEncodedPathThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowSemicolon(true);
this.request = get("/context-root/a/b;%2F1/c");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenAllowUrlLowerCaseEncodedDoubleSlashThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowUrlEncodedDoubleSlash(true);
this.request = get("/context-root/a/b%2f%2fc");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenAllowUrlUpperCaseEncodedDoubleSlashThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowUrlEncodedDoubleSlash(true);
this.request = get("/context-root/a/b%2F%2Fc");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenAllowUrlLowerCaseAndUpperCaseEncodedDoubleSlashThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowUrlEncodedDoubleSlash(true);
this.request = get("/context-root/a/b%2f%2Fc");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenAllowUrlUpperCaseAndLowerCaseEncodedDoubleSlashThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowUrlEncodedDoubleSlash(true);
this.request = get("/context-root/a/b%2F%2fc");
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenRemoveFromUpperCaseEncodedUrlBlocklistThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.request = get("/context-root/a/b%2Fc");
this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F%2F"));
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenRemoveFromDecodedUrlBlocklistThenNoException() {
this.request = get("/a/b%2F%2Fc");
this.firewall.getDecodedUrlBlocklist().removeAll(Arrays.asList("//"));
this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F%2F"));
this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F"));
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenTrustedDomainThenNoException() {
this.request.header("Host", "example.org");
this.firewall.setAllowedHostnames((hostname) -> hostname.equals("example.org"));
getFirewalledExchange();
}
@Test
void getFirewalledExchangeWhenUntrustedDomainThenException() {
this.request = get("https://example.org");
this.firewall.setAllowedHostnames((hostname) -> hostname.equals("myexample.org"));
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
}
@Test
void getFirewalledExchangeGetHeaderWhenNotAllowedHeaderNameThenException() {
this.firewall.setAllowedHeaderNames((name) -> !name.equals("bad name"));
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("bad name"));
}
@Test
void getFirewalledExchangeWhenHeaderNameNotAllowedWithAugmentedHeaderNamesThenException() {
this.firewall.setAllowedHeaderNames(
StrictServerWebExchangeFirewall.ALLOWED_HEADER_NAMES.and((name) -> !name.equals("bad name")));
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.getFirst("bad name"));
}
@Test
void getFirewalledExchangeGetHeaderWhenNotAllowedHeaderValueThenException() {
this.request.header("good name", "bad value");
this.firewall.setAllowedHeaderValues((value) -> !value.equals("bad value"));
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("good name"));
}
@Test
void getFirewalledExchangeWhenHeaderValueNotAllowedWithAugmentedHeaderValuesThenException() {
this.request.header("good name", "bad value");
this.firewall.setAllowedHeaderValues(
StrictServerWebExchangeFirewall.ALLOWED_HEADER_VALUES.and((value) -> !value.equals("bad value")));
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("good name"));
}
@Test
void getFirewalledExchangeGetDateHeaderWhenControlCharacterInHeaderNameThenException() {
this.request.header("Bad\0Name", "some value");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Bad\0Name"));
}
@Test
void getFirewalledExchangeGetHeaderWhenUndefinedCharacterInHeaderNameThenException() {
this.request.header("Bad\uFFFEName", "some value");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Bad\uFFFEName"));
}
@Test
void getFirewalledExchangeGetHeadersWhenControlCharacterInHeaderNameThenException() {
this.request.header("Bad\0Name", "some value");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Bad\0Name"));
}
@Test
void getFirewalledExchangeGetHeaderNamesWhenControlCharacterInHeaderNameThenException() {
this.request.header("Bad\0Name", "some value");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class)
.isThrownBy(() -> headers.keySet().iterator().next());
}
@Test
void getFirewalledExchangeGetHeaderWhenControlCharacterInHeaderValueThenException() {
this.request.header("Something", "bad\0value");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Something"));
}
@Test
void getFirewalledExchangeGetHeaderWhenHorizontalTabInHeaderValueThenNoException() {
this.request.header("Something", "tab\tvalue");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThat(headers.getFirst("Something")).isEqualTo("tab\tvalue");
}
@Test
void getFirewalledExchangeGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
this.request.header("Something", "bad\uFFFEvalue");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class)
.isThrownBy(() -> headers.getFirst("Something"));
}
@Test
void getFirewalledExchangeGetHeadersWhenControlCharacterInHeaderValueThenException() {
this.request.header("Something", "bad\0value");
ServerWebExchange exchange = getFirewalledExchange();
HttpHeaders headers = exchange.getRequest().getHeaders();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Something"));
}
@Test
void getFirewalledExchangeGetParameterWhenControlCharacterInParameterNameThenException() {
this.request.queryParam("Bad\0Name", "some value");
ServerWebExchange exchange = getFirewalledExchange();
ServerHttpRequest request = exchange.getRequest();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(request::getQueryParams);
}
@Test
void getFirewalledExchangeGetParameterValuesWhenNotAllowedInParameterValueThenException() {
this.firewall.setAllowedParameterValues((value) -> !value.equals("bad value"));
this.request.queryParam("Something", "bad value");
ServerWebExchange exchange = getFirewalledExchange();
ServerHttpRequest request = exchange.getRequest();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> request.getQueryParams());
}
@Test
void getFirewalledExchangeGetParameterValuesWhenNotAllowedInParameterNameThenException() {
this.firewall.setAllowedParameterNames((value) -> !value.equals("bad name"));
this.request.queryParam("bad name", "good value");
ServerWebExchange exchange = getFirewalledExchange();
ServerHttpRequest request = exchange.getRequest();
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> request.getQueryParams());
}
// gh-9598
@Test
void getFirewalledExchangeGetHeaderWhenNameIsNullThenNull() {
ServerWebExchange exchange = getFirewalledExchange();
assertThat(exchange.getRequest().getHeaders().get(null)).isNull();
}
}