Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85ca9e9a57 | |||
| d9e9e3cdeb | |||
| 3e43eda42b | |||
| e7d6dc22b2 | |||
| da06f6a9e6 | |||
| 6a0b683e60 | |||
| 2b6d586987 | |||
| 8d1b0d9764 | |||
| 2fe7faea87 | |||
| eb313ec901 | |||
| 7873ab8601 | |||
| 348f064df1 | |||
| 8b9fe13c88 | |||
| 1f7dcc0fa6 | |||
| 908b9b5a85 | |||
| 807c3dd3ab | |||
| cac03995a3 | |||
| 6ea7da5178 | |||
| 49f1b3554c | |||
| 519df3f7d5 | |||
| 16272f634c | |||
| 0b9887505e | |||
| dd8ee38194 | |||
| 87de6cea1b | |||
| 3d1e4b5f18 | |||
| ef7b11ac01 | |||
| 0f85da77be | |||
| 96a9cf0d2d | |||
| ebb75f5e90 | |||
| a6c3d123ed | |||
| 324de7af93 | |||
| e79ceaeb75 | |||
| f361ee72e8 | |||
| 21ac1022ef | |||
| 68d91916e2 | |||
| 753f8aecc9 | |||
| 84e4c9bc42 | |||
| 1dbaa08cd4 | |||
| 1d916c35d1 | |||
| e8ba039a61 | |||
| 4b41f8cb5b | |||
| 0eb6acde96 | |||
| 73f3f75712 | |||
| 70a950119c | |||
| b8e9f47dd4 | |||
| a0a9b48224 | |||
| 9363959566 | |||
| f38129b5a0 | |||
| 6542509207 | |||
| 7d55c079c9 | |||
| e44fc3817a | |||
| 1414b88ef3 | |||
| ba520db7f7 | |||
| 85248083c0 | |||
| a91433d52b | |||
| d34bd346c0 | |||
| be09ed7e2b | |||
| d224dbe334 | |||
| c62168ca5b | |||
| d0b2b33dce | |||
| 7881660ca0 | |||
| 96c47b30d3 | |||
| 506e5b7f11 | |||
| a5cd7ce122 | |||
| fa5fc6dd62 | |||
| 709103e38c | |||
| a8c4d6cead | |||
| a7bf8f7cc6 | |||
| 0c6b0748b9 | |||
| 5f838b0e93 | |||
| d1115d2fbd | |||
| 82060c3b95 | |||
| bc3e6f2919 | |||
| 02c6e173d7 | |||
| 451fbf0227 | |||
| 85deaf7b8b | |||
| 810d83e2f8 | |||
| ae893ec872 | |||
| 285d16b046 | |||
| ddf4542a9e | |||
| 554df6fab6 | |||
| 0eaffb37e7 | |||
| f197f21054 | |||
| 222e26133b | |||
| d627120856 | |||
| 127ed4b7cf | |||
| fcddb2e56f | |||
| da345a3bac | |||
| d985d044bc | |||
| f08c7a9e48 | |||
| 89bfdb109a | |||
| 0790978590 | |||
| 2a27b3ef7b | |||
| 0d8b8ee04b | |||
| f1ef152b7e | |||
| e1ad989d38 | |||
| 81e74e65d4 | |||
| c24b5ebe98 | |||
| fd900c288e | |||
| e86d88d0cf | |||
| b9d5493913 | |||
| 86f3cd6dc7 | |||
| 4c6fef82b9 | |||
| cd9339dc7c | |||
| e435d0f0eb | |||
| 0adc743bc0 | |||
| 18129f3af3 | |||
| f28c26fd54 | |||
| f532481841 | |||
| 421430330a | |||
| 1da930ec6e | |||
| fed3c99c1d | |||
| d9d77bed82 | |||
| 40da492609 | |||
| 5c2106b22e | |||
| a9be3840f8 | |||
| 1ba6301afa | |||
| 48241deba3 | |||
| 9cc11be9f3 | |||
| adc66e134b | |||
| 308e408b13 | |||
| 3ba1263d64 | |||
| 5048a68ab7 | |||
| addc7c53b2 | |||
| 1399a82ea9 | |||
| 0106e4b299 | |||
| 3592253b8e | |||
| 9558b70616 | |||
| 0b83830fb3 | |||
| ec33e40748 | |||
| c104f44546 | |||
| 18dba34bde | |||
| 4ac092c803 | |||
| e48d6b039b | |||
| c702c9918c | |||
| e9bbe31b1a | |||
| dd3c6892e9 | |||
| f9f533499b | |||
| 7912c574ca | |||
| 7209155482 | |||
| c552366a78 | |||
| 72a208e02c | |||
| 77afcd3e53 | |||
| 7215c72373 | |||
| 542071b1f8 | |||
| 4ce7cde155 |
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -2,9 +2,6 @@ name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
+3
-1
@@ -18,6 +18,7 @@ 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;
|
||||
|
||||
@@ -73,7 +74,8 @@ public final class GrantedAuthorityFromAssertionAttributesUserDetailsService
|
||||
}
|
||||
|
||||
private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) {
|
||||
return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString());
|
||||
return new SimpleGrantedAuthority(
|
||||
this.convertToUpperCase ? o.toString().toUpperCase(Locale.ROOT) : o.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-49
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authentication.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -63,62 +62,24 @@ class InitializeAuthenticationProviderBeanManagerConfigurer extends GlobalAuthen
|
||||
if (auth.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
List<BeanWithName<AuthenticationProvider>> authenticationProviders = getBeansWithName(
|
||||
AuthenticationProvider.class);
|
||||
if (authenticationProviders.isEmpty()) {
|
||||
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(AuthenticationProvider.class);
|
||||
if (beanNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
else if (authenticationProviders.size() > 1) {
|
||||
List<String> beanNames = authenticationProviders.stream().map(BeanWithName::getName).toList();
|
||||
else if (beanNames.length > 1) {
|
||||
this.logger.info(LogMessage.format("Found %s AuthenticationProvider beans, with names %s. "
|
||||
+ "Global Authentication Manager will not be configured with AuthenticationProviders. "
|
||||
+ "Consider publishing a single AuthenticationProvider bean, or wiring your Providers directly "
|
||||
+ "using the DSL.", authenticationProviders.size(), beanNames));
|
||||
+ "using the DSL.", beanNames.length, Arrays.toString(beanNames)));
|
||||
return;
|
||||
}
|
||||
AuthenticationProvider authenticationProvider = authenticationProviders.get(0).getBean();
|
||||
String authenticationProviderBeanName = authenticationProviders.get(0).getName();
|
||||
|
||||
AuthenticationProvider authenticationProvider = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBean(beanNames[0], AuthenticationProvider.class);
|
||||
auth.authenticationProvider(authenticationProvider);
|
||||
this.logger.info(LogMessage.format(
|
||||
"Global AuthenticationManager configured with AuthenticationProvider bean with name %s",
|
||||
authenticationProviderBeanName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of beans of the requested class, along with their names. If
|
||||
* there are no registered beans of that type, the list is empty.
|
||||
*/
|
||||
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
|
||||
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
|
||||
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(type);
|
||||
for (String beanName : beanNames) {
|
||||
T bean = InitializeAuthenticationProviderBeanManagerConfigurer.this.context.getBean(beanName, type);
|
||||
beanWithNames.add(new BeanWithName<T>(bean, beanName));
|
||||
}
|
||||
return beanWithNames;
|
||||
}
|
||||
|
||||
static class BeanWithName<T> {
|
||||
|
||||
private final T bean;
|
||||
|
||||
private final String name;
|
||||
|
||||
BeanWithName(T bean, String name) {
|
||||
this.bean = bean;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
T getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
beanNames[0]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-48
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authentication.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -66,9 +65,10 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
|
||||
@Override
|
||||
public void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
List<BeanWithName<UserDetailsService>> userDetailsServices = getBeansWithName(UserDetailsService.class);
|
||||
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(UserDetailsService.class);
|
||||
if (auth.isConfigured()) {
|
||||
if (!userDetailsServices.isEmpty()) {
|
||||
if (beanNames.length > 0) {
|
||||
this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. "
|
||||
+ "UserDetailsService beans will not be used for username/password login. "
|
||||
+ "Consider removing the AuthenticationProvider bean. "
|
||||
@@ -78,19 +78,18 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
return;
|
||||
}
|
||||
|
||||
if (userDetailsServices.isEmpty()) {
|
||||
if (beanNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
else if (userDetailsServices.size() > 1) {
|
||||
List<String> beanNames = userDetailsServices.stream().map(BeanWithName::getName).toList();
|
||||
else if (beanNames.length > 1) {
|
||||
this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. "
|
||||
+ "Global Authentication Manager will not use a UserDetailsService for username/password login. "
|
||||
+ "Consider publishing a single UserDetailsService bean.", userDetailsServices.size(),
|
||||
beanNames));
|
||||
+ "Consider publishing a single UserDetailsService bean.", beanNames.length,
|
||||
Arrays.toString(beanNames)));
|
||||
return;
|
||||
}
|
||||
UserDetailsService userDetailsService = userDetailsServices.get(0).getBean();
|
||||
String userDetailsServiceBeanName = userDetailsServices.get(0).getName();
|
||||
UserDetailsService userDetailsService = InitializeUserDetailsBeanManagerConfigurer.this.context
|
||||
.getBean(beanNames[0], UserDetailsService.class);
|
||||
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
|
||||
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
|
||||
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class);
|
||||
@@ -111,8 +110,7 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
provider.afterPropertiesSet();
|
||||
auth.authenticationProvider(provider);
|
||||
this.logger.info(LogMessage.format(
|
||||
"Global AuthenticationManager configured with UserDetailsService bean with name %s",
|
||||
userDetailsServiceBeanName));
|
||||
"Global AuthenticationManager configured with UserDetailsService bean with name %s", beanNames[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,41 +125,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of beans of the requested class, along with their names. If
|
||||
* there are no registered beans of that type, the list is empty.
|
||||
*/
|
||||
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
|
||||
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
|
||||
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
|
||||
for (String beanName : beanNames) {
|
||||
T bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanName, type);
|
||||
beanWithNames.add(new BeanWithName<T>(bean, beanName));
|
||||
}
|
||||
return beanWithNames;
|
||||
}
|
||||
|
||||
static class BeanWithName<T> {
|
||||
|
||||
private final T bean;
|
||||
|
||||
private final String name;
|
||||
|
||||
BeanWithName(T bean, String name) {
|
||||
this.bean = bean;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
T getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -174,7 +174,8 @@ 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();
|
||||
RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build());
|
||||
AuthorizationManager<HttpServletRequest> manager = postProcess(
|
||||
(AuthorizationManager<HttpServletRequest>) this.managerBuilder.build());
|
||||
if (registry.isNoop()) {
|
||||
return manager;
|
||||
}
|
||||
|
||||
+7
-1
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -33,6 +34,8 @@ 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;
|
||||
@@ -79,11 +82,14 @@ class WebFluxSecurityConfiguration {
|
||||
|
||||
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
|
||||
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
|
||||
WebFilterChainProxy springSecurityWebFilterChainFilter() {
|
||||
WebFilterChainProxy springSecurityWebFilterChainFilter(ObjectProvider<ServerWebExchangeFirewall> firewall,
|
||||
ObjectProvider<ServerExchangeRejectedHandler> rejectedHandler) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -313,7 +314,7 @@ class HttpConfigurationBuilder {
|
||||
|
||||
// Needed to account for placeholders
|
||||
static String createPath(String path, boolean lowerCase) {
|
||||
return lowerCase ? path.toLowerCase() : path;
|
||||
return lowerCase ? path.toLowerCase(Locale.ENGLISH) : path;
|
||||
}
|
||||
|
||||
BeanMetadataElement getSecurityContextHolderStrategyForAuthenticationFilters() {
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+30
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
|
||||
import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
|
||||
import com.nimbusds.jose.proc.JWKSecurityContext;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
@@ -23,19 +27,22 @@ import org.springframework.security.authentication.AuthenticationServiceExceptio
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.jwt.BadJwtException;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
|
||||
@@ -61,9 +68,27 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti
|
||||
* Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager}
|
||||
*/
|
||||
OidcBackChannelLogoutReactiveAuthenticationManager() {
|
||||
ReactiveOidcIdTokenDecoderFactory logoutTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
|
||||
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
|
||||
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
|
||||
DefaultOidcLogoutTokenValidatorFactory jwtValidator = new DefaultOidcLogoutTokenValidatorFactory();
|
||||
this.logoutTokenDecoderFactory = (clientRegistration) -> {
|
||||
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
|
||||
if (!StringUtils.hasText(jwkSetUri)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
|
||||
"Failed to find a Signature Verifier for Client Registration: '"
|
||||
+ clientRegistration.getRegistrationId()
|
||||
+ "'. Check to ensure you have configured the JwkSet URI.",
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
JOSEObjectTypeVerifier<JWKSecurityContext> typeVerifier = new DefaultJOSEObjectTypeVerifier<>(null,
|
||||
JOSEObjectType.JWT, new JOSEObjectType("logout+jwt"));
|
||||
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
|
||||
.jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(typeVerifier))
|
||||
.build();
|
||||
decoder.setJwtValidator(jwtValidator.apply(clientRegistration));
|
||||
decoder.setClaimSetConverter(
|
||||
new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
|
||||
return decoder;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+15
-18
@@ -16,16 +16,17 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
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.io.buffer.DataBuffer;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
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.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
@@ -62,6 +63,9 @@ 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
|
||||
@@ -84,7 +88,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||
})
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap(this.authenticationManager::authenticate)
|
||||
@@ -93,7 +97,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||
})
|
||||
.flatMap((authentication) -> {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
@@ -101,19 +105,12 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
|
||||
private Mono<Void> handleAuthenticationFailure(ServerWebExchange exchange, Exception ex) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
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));
|
||||
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());
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Exception ex) {
|
||||
|
||||
+15
-16
@@ -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,14 +25,15 @@ 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.io.buffer.DataBuffer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
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;
|
||||
@@ -44,6 +45,7 @@ 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;
|
||||
|
||||
@@ -63,6 +65,9 @@ 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";
|
||||
@@ -97,7 +102,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
totalCount.intValue()));
|
||||
}
|
||||
if (!list.isEmpty()) {
|
||||
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
|
||||
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
@@ -148,17 +153,11 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
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));
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -155,6 +155,7 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
+78
@@ -16,14 +16,26 @@
|
||||
|
||||
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;
|
||||
|
||||
@@ -47,6 +59,46 @@ 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
|
||||
@@ -57,6 +109,32 @@ 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 {
|
||||
|
||||
|
||||
+18
-8
@@ -50,6 +50,7 @@ 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;
|
||||
@@ -74,6 +75,8 @@ import org.springframework.security.oauth2.client.registration.TestClientRegistr
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
@@ -97,6 +100,7 @@ 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;
|
||||
@@ -195,7 +199,10 @@ public class OidcLogoutSpecTests {
|
||||
.body(BodyInserters.fromFormData("logout_token", "invalid"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest();
|
||||
.isBadRequest()
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||
})
|
||||
.value(hasValue("invalid_request"));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@@ -262,9 +269,10 @@ public class OidcLogoutSpecTests {
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest()
|
||||
.expectBody(String.class)
|
||||
.value(containsString("partial_logout"))
|
||||
.value(containsString("not all sessions were terminated"));
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||
})
|
||||
.value(hasValue("partial_logout"))
|
||||
.value(hasValue(containsString("not all sessions were terminated")));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@@ -694,8 +702,9 @@ public class OidcLogoutSpecTests {
|
||||
String logoutToken(@AuthenticationPrincipal OidcUser user) {
|
||||
OidcLogoutToken token = TestOidcLogoutTokens.withUser(user)
|
||||
.audience(List.of(this.registration.getClientId())).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||
.from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build());
|
||||
JwsHeader header = JwsHeader.with(SignatureAlgorithm.RS256).type("logout+jwt").build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder().claims((c) -> c.putAll(token.getClaims())).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters.from(header, claims);
|
||||
return this.encoder.encode(parameters).getTokenValue();
|
||||
}
|
||||
|
||||
@@ -704,8 +713,9 @@ public class OidcLogoutSpecTests {
|
||||
OidcLogoutToken token = TestOidcLogoutTokens.withUser(user)
|
||||
.audience(List.of(this.registration.getClientId()))
|
||||
.claims((claims) -> claims.remove(LogoutTokenClaimNames.SID)).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||
.from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build());
|
||||
JwsHeader header = JwsHeader.with(SignatureAlgorithm.RS256).type("JWT").build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder().claims((c) -> c.putAll(token.getClaims())).build();
|
||||
JwtEncoderParameters parameters = JwtEncoderParameters.from(header, claims);
|
||||
return this.encoder.encode(parameters).getTokenValue();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.authentication;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import io.micrometer.common.KeyValues;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
@@ -53,7 +55,7 @@ public final class AuthenticationObservationConvention
|
||||
if (authenticationType.endsWith("Authentication")) {
|
||||
authenticationType = authenticationType.substring(0, authenticationType.lastIndexOf("Authentication"));
|
||||
}
|
||||
return "authenticate " + authenticationType.toLowerCase();
|
||||
return "authenticate " + authenticationType.toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
return "authenticate";
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* 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.
|
||||
@@ -79,10 +79,10 @@ public class SimpleAttributes2GrantedAuthoritiesMapper
|
||||
*/
|
||||
private GrantedAuthority getGrantedAuthority(String attribute) {
|
||||
if (isConvertAttributeToLowerCase()) {
|
||||
attribute = attribute.toLowerCase(Locale.getDefault());
|
||||
attribute = attribute.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
else if (isConvertAttributeToUpperCase()) {
|
||||
attribute = attribute.toUpperCase(Locale.getDefault());
|
||||
attribute = attribute.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
if (isAddPrefixIfAlreadyExisting() || !attribute.startsWith(getAttributePrefix())) {
|
||||
return new SimpleGrantedAuthority(getAttributePrefix() + attribute);
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -71,10 +72,10 @@ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, In
|
||||
|
||||
private GrantedAuthority mapAuthority(String name) {
|
||||
if (this.convertToUpperCase) {
|
||||
name = name.toUpperCase();
|
||||
name = name.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
else if (this.convertToLowerCase) {
|
||||
name = name.toLowerCase();
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
if (this.prefix.length() > 0 && !name.startsWith(this.prefix)) {
|
||||
name = this.prefix + name;
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
|
||||
@@ -91,7 +92,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService
|
||||
}
|
||||
|
||||
private String getKey(String username) {
|
||||
return username.toLowerCase();
|
||||
return username.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@ 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;
|
||||
|
||||
@@ -45,10 +46,10 @@ public class UserAttributeEditor extends PropertyEditorSupport {
|
||||
userAttrib.setPassword(currentToken);
|
||||
}
|
||||
else {
|
||||
if (currentToken.toLowerCase().equals("enabled")) {
|
||||
if (currentToken.toLowerCase(Locale.ENGLISH).equals("enabled")) {
|
||||
userAttrib.setEnabled(true);
|
||||
}
|
||||
else if (currentToken.toLowerCase().equals("disabled")) {
|
||||
else if (currentToken.toLowerCase(Locale.ENGLISH).equals("disabled")) {
|
||||
userAttrib.setEnabled(false);
|
||||
}
|
||||
else {
|
||||
|
||||
+8
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
|
||||
@@ -96,23 +97,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(), new MutableUser(user));
|
||||
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(String username) {
|
||||
this.users.remove(username.toLowerCase());
|
||||
this.users.remove(username.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUser(UserDetails user) {
|
||||
Assert.isTrue(userExists(user.getUsername()), "user should exist");
|
||||
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
|
||||
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userExists(String username) {
|
||||
return this.users.containsKey(username.toLowerCase());
|
||||
return this.users.containsKey(username.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,14 +144,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());
|
||||
MutableUserDetails mutableUser = this.users.get(username.toLowerCase(Locale.ROOT));
|
||||
mutableUser.setPassword(newPassword);
|
||||
return mutableUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserDetails user = this.users.get(username.toLowerCase());
|
||||
UserDetails user = this.users.get(username.toLowerCase(Locale.ROOT));
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -50,11 +51,11 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final String SSHA_PREFIX = "{SSHA}";
|
||||
|
||||
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
|
||||
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
private static final String SHA_PREFIX = "{SHA}";
|
||||
|
||||
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
|
||||
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
private BytesKeyGenerator saltGenerator;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -200,3 +200,35 @@ firewall.setAllowedHeaderValues {
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
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,6 +14,7 @@ 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.
|
||||
@@ -66,6 +67,7 @@ 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~__.
|
||||
@@ -113,6 +115,7 @@ The following image shows the role of `FilterChainProxy`.
|
||||
|
||||
.FilterChainProxy
|
||||
[[servlet-filterchainproxy-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/filterchainproxy.png[]
|
||||
|
||||
[[servlet-securityfilterchain]]
|
||||
@@ -124,6 +127,7 @@ 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>>.
|
||||
@@ -145,6 +149,7 @@ 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.
|
||||
@@ -398,6 +403,7 @@ 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,6 +22,7 @@ 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].
|
||||
@@ -175,6 +176,7 @@ 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.
|
||||
@@ -184,11 +186,13 @@ 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]]
|
||||
@@ -234,6 +238,7 @@ 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.
|
||||
|
||||
@@ -315,7 +315,7 @@ While using `logoutSuccessUrl` will suffice for most cases, you may need to do s
|
||||
For example, instead of redirecting, you may want to only return a status code.
|
||||
In this case, you can provide a success handler instance, like so:
|
||||
|
||||
.Using Clear-Site-Data to Clear Cookies
|
||||
.Using HttpStatusReturningLogoutSuccessHandler to return a status code
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.
|
||||
@@ -26,6 +27,7 @@ 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.
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ 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,6 +10,7 @@ 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.
|
||||
@@ -30,6 +31,7 @@ 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,6 +191,7 @@ 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`.
|
||||
@@ -212,6 +213,7 @@ 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,6 +126,7 @@ 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.
|
||||
@@ -342,6 +343,7 @@ 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.
|
||||
@@ -402,6 +404,7 @@ 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,6 +65,7 @@ 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].
|
||||
|
||||
@@ -115,6 +115,7 @@ 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,6 +82,7 @@ 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:
|
||||
@@ -90,6 +91,7 @@ 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,6 +205,78 @@ 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,6 +21,7 @@ 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.
|
||||
@@ -38,6 +39,7 @@ 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.
|
||||
|
||||
@@ -92,6 +92,7 @@ Let's take a look at how `JwtAuthenticationProvider` works within Spring Securit
|
||||
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.
|
||||
|
||||
.`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`].
|
||||
|
||||
@@ -88,6 +88,7 @@ Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring
|
||||
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.
|
||||
|
||||
.`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`].
|
||||
|
||||
@@ -7,6 +7,7 @@ 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]
|
||||
@@ -34,6 +35,7 @@ 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
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"antora": "3.2.0-alpha.8",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-beta.3",
|
||||
"@antora/collector-extension": "1.0.1",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
|
||||
|
||||
@@ -40,4 +40,8 @@
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -30,5 +30,21 @@
|
||||
<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
@@ -1,5 +1,5 @@
|
||||
springBootVersion=3.1.1
|
||||
version=6.3.4
|
||||
version=6.3.6
|
||||
samplesBranch=6.3.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+10
-10
@@ -13,11 +13,11 @@ org-jetbrains-kotlin = "1.9.25"
|
||||
org-jetbrains-kotlinx = "1.8.1"
|
||||
org-mockito = "5.11.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-springframework = "6.1.14"
|
||||
org-springframework = "6.1.16"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.11"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.2"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.3"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.3"
|
||||
@@ -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.11"
|
||||
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.11"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.13"
|
||||
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,7 +68,7 @@ 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.4.10.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
|
||||
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" }
|
||||
@@ -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:2024.0.5"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.7"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.7"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.10"
|
||||
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"
|
||||
|
||||
@@ -95,11 +95,11 @@ net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8"
|
||||
org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33"
|
||||
org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.3.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.5"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.6"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.22"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.23"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2005-2010 the original author or authors.
|
||||
* Copyright 2005-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.ldap;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.ldap.BadLdapGrammarException;
|
||||
|
||||
/**
|
||||
@@ -72,7 +74,7 @@ final class LdapEncoder {
|
||||
}
|
||||
|
||||
protected static String toTwoCharHex(char c) {
|
||||
String raw = Integer.toHexString(c).toUpperCase();
|
||||
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
|
||||
return (raw.length() > 1) ? raw : "0" + raw;
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2005-2010 the original author or authors.
|
||||
* Copyright 2005-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.ldap.authentication;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.ldap.BadLdapGrammarException;
|
||||
|
||||
/**
|
||||
@@ -72,7 +74,7 @@ final class LdapEncoder {
|
||||
}
|
||||
|
||||
protected static String toTwoCharHex(char c) {
|
||||
String raw = Integer.toHexString(c).toUpperCase();
|
||||
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
|
||||
return (raw.length() > 1) ? raw : "0" + raw;
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -20,6 +20,7 @@ import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -142,9 +143,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() : null;
|
||||
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
|
||||
this.url = url;
|
||||
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null;
|
||||
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase(Locale.ROOT) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +154,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() : null;
|
||||
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
|
||||
this.url = url;
|
||||
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null;
|
||||
}
|
||||
@@ -336,7 +337,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
|
||||
}
|
||||
|
||||
String createBindPrincipal(String username) {
|
||||
if (this.domain == null || username.toLowerCase().endsWith(this.domain)) {
|
||||
if (this.domain == null || username.toLowerCase(Locale.ROOT).endsWith(this.domain)) {
|
||||
return username;
|
||||
}
|
||||
return username + "@" + this.domain;
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ 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;
|
||||
@@ -179,7 +180,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
return null;
|
||||
}
|
||||
if (this.convertToUpperCase) {
|
||||
role = role.toUpperCase();
|
||||
role = role.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role);
|
||||
};
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -124,7 +125,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());
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase(Locale.ROOT));
|
||||
};
|
||||
|
||||
private String[] attributesToRetrieve;
|
||||
@@ -287,7 +288,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
|
||||
*/
|
||||
protected DistinguishedName buildGroupDn(String group) {
|
||||
DistinguishedName dn = new DistinguishedName(this.groupSearchBase);
|
||||
dn.add(this.groupRoleAttributeName, group.toLowerCase());
|
||||
dn.add(this.groupRoleAttributeName, group.toLowerCase(Locale.ROOT));
|
||||
return dn;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
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;
|
||||
@@ -127,7 +128,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
|
||||
protected GrantedAuthority createAuthority(Object role) {
|
||||
if (role instanceof String) {
|
||||
if (this.convertToUpperCase) {
|
||||
role = ((String) role).toUpperCase();
|
||||
role = ((String) role).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role);
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
|
||||
@@ -193,7 +194,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
|
||||
}
|
||||
for (String role : roles) {
|
||||
if (isConvertToUpperCase()) {
|
||||
role = role.toUpperCase();
|
||||
role = role.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
role = getRolePrefix() + role;
|
||||
// if the group already exist, we will not search for it's parents again.
|
||||
|
||||
+8
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -80,7 +80,13 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
|
||||
if (registration == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+10
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -62,8 +62,15 @@ 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)
|
||||
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
|
||||
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
|
||||
.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());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -539,7 +540,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
// @formatter:off
|
||||
return Stream.of(wwwAuthenticateHeader)
|
||||
.filter((header) -> StringUtils.hasLength(header))
|
||||
.filter((header) -> header.toLowerCase().startsWith("bearer"))
|
||||
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
|
||||
.map((header) -> header.substring("bearer".length()))
|
||||
.map((header) -> header.split(","))
|
||||
.flatMap(Stream::of)
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -654,7 +655,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().startsWith("bearer"))
|
||||
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
|
||||
.map((header) -> header.substring("bearer".length()))
|
||||
.map((header) -> header.split(","))
|
||||
.flatMap(Stream::of)
|
||||
|
||||
+72
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,22 +18,25 @@ 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.Mockito.mock;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link InMemoryOAuth2AuthorizedClientService}.
|
||||
@@ -79,9 +82,11 @@ 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),
|
||||
mock(OAuth2AuthorizedClient.class));
|
||||
authorizedClient);
|
||||
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
|
||||
given(clientRegistrationRepository.findByRegistrationId(eq(registrationId))).willReturn(this.registration3);
|
||||
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
|
||||
@@ -124,7 +129,35 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
|
||||
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
|
||||
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
|
||||
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
|
||||
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
|
||||
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));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,7 +181,7 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
|
||||
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
|
||||
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
|
||||
.loadAuthorizedClient(this.registration3.getRegistrationId(), this.principalName2);
|
||||
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
|
||||
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,4 +213,38 @@ 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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+74
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,12 +18,14 @@ 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;
|
||||
|
||||
@@ -34,7 +36,9 @@ 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;
|
||||
|
||||
@@ -56,8 +60,9 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
|
||||
|
||||
private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused");
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(),
|
||||
Instant.now().plus(Duration.ofDays(1)));
|
||||
private OAuth2AccessToken accessToken;
|
||||
|
||||
private OAuth2RefreshToken refreshToken;
|
||||
|
||||
// @formatter:off
|
||||
private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId)
|
||||
@@ -79,6 +84,11 @@ 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
|
||||
@@ -153,11 +163,37 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
|
||||
.saveAuthorizedClient(authorizedClient, this.principal)
|
||||
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
|
||||
StepVerifier.create(saveAndLoad)
|
||||
.expectNext(authorizedClient)
|
||||
.assertNext(isEqualTo(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;
|
||||
@@ -246,4 +282,38 @@ 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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -57,13 +57,18 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
|
||||
? resolveFromRequestParameters(request) : null;
|
||||
if (authorizationHeaderToken != null) {
|
||||
if (parameterToken != null) {
|
||||
final BearerTokenError error = BearerTokenErrors
|
||||
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;
|
||||
|
||||
+6
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -78,6 +78,11 @@ 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;
|
||||
|
||||
+35
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,8 +21,11 @@ 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;
|
||||
@@ -258,4 +261,35 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -187,9 +187,9 @@ public class ServerBearerTokenAuthenticationConverterTests {
|
||||
.isThrownBy(() -> convertToToken(request))
|
||||
.satisfies((ex) -> {
|
||||
BearerTokenError error = (BearerTokenError) ex.getError();
|
||||
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_TOKEN);
|
||||
assertThat(error.getErrorCode()).isEqualTo(BearerTokenErrorCodes.INVALID_REQUEST);
|
||||
assertThat(error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
assertThat(error.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
});
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004-2022 the original author or authors.
|
||||
* Copyright 2004-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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.taglibs.authz;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
@@ -169,7 +170,7 @@ public abstract class AbstractAuthorizeTag {
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
this.method = (method != null) ? method.toUpperCase() : null;
|
||||
this.method = (method != null) ? method.toUpperCase(Locale.ENGLISH) : null;
|
||||
}
|
||||
|
||||
private SecurityContext getContext() {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import jakarta.servlet.ServletRequest;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
@@ -45,7 +47,7 @@ public class PortResolverImpl implements PortResolver {
|
||||
@Override
|
||||
public int getServerPort(ServletRequest request) {
|
||||
int serverPort = request.getServerPort();
|
||||
String scheme = request.getScheme().toLowerCase();
|
||||
String scheme = request.getScheme().toLowerCase(Locale.ENGLISH);
|
||||
Integer mappedPort = getMappedPort(serverPort, scheme);
|
||||
return (mappedPort != null) ? mappedPort : serverPort;
|
||||
}
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.web.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -38,7 +37,7 @@ public final class DelegatingAuthenticationConverter implements AuthenticationCo
|
||||
|
||||
public DelegatingAuthenticationConverter(List<AuthenticationConverter> delegates) {
|
||||
Assert.notEmpty(delegates, "delegates cannot be null");
|
||||
this.delegates = new ArrayList<>(delegates);
|
||||
this.delegates = List.copyOf(delegates);
|
||||
}
|
||||
|
||||
public DelegatingAuthenticationConverter(AuthenticationConverter... delegates) {
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -63,7 +64,7 @@ public final class HaveIBeenPwnedRestApiPasswordChecker implements CompromisedPa
|
||||
@NonNull
|
||||
public CompromisedPasswordDecision check(String password) {
|
||||
byte[] hash = this.sha1Digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
String encoded = new String(Hex.encode(hash)).toUpperCase();
|
||||
String encoded = new String(Hex.encode(hash)).toUpperCase(Locale.ROOT);
|
||||
String prefix = encoded.substring(0, PREFIX_LENGTH);
|
||||
String suffix = encoded.substring(PREFIX_LENGTH);
|
||||
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@ package org.springframework.security.web.authentication.password;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -67,8 +68,8 @@ public class HaveIBeenPwnedRestApiReactivePasswordChecker implements ReactiveCom
|
||||
}
|
||||
|
||||
private Mono<Boolean> findLeakedPassword(String encodedPassword) {
|
||||
String prefix = encodedPassword.substring(0, PREFIX_LENGTH).toUpperCase();
|
||||
String suffix = encodedPassword.substring(PREFIX_LENGTH).toUpperCase();
|
||||
String prefix = encodedPassword.substring(0, PREFIX_LENGTH).toUpperCase(Locale.ROOT);
|
||||
String suffix = encodedPassword.substring(PREFIX_LENGTH).toUpperCase(Locale.ROOT);
|
||||
return getLeakedPasswordsForPrefix(prefix).any((leakedPw) -> leakedPw.startsWith(suffix));
|
||||
}
|
||||
|
||||
|
||||
+14
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-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.
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -97,8 +98,18 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
|
||||
|
||||
this.cookieCustomizer.accept(cookieBuilder);
|
||||
|
||||
Cookie cookie = mapToCookie(cookieBuilder.build());
|
||||
response.addCookie(cookie);
|
||||
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());
|
||||
}
|
||||
|
||||
// Set request attribute to signal that response has blank cookie value,
|
||||
// which allows loadToken to return null when token has been removed
|
||||
|
||||
+12
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -73,6 +73,9 @@ 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);
|
||||
@@ -122,8 +125,14 @@ public class CookieRequestCache implements RequestCache {
|
||||
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
|
||||
}
|
||||
|
||||
private static String decodeCookie(String encodedCookieValue) {
|
||||
return new String(Base64.getDecoder().decode(encodedCookieValue.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 getCookiePath(HttpServletRequest request) {
|
||||
|
||||
+64
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.web.server.firewall;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -23,6 +25,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -33,6 +36,7 @@ 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.http.server.reactive.SslInfo;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -743,6 +747,11 @@ public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewal
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mutate() {
|
||||
return new StrictFirewallBuilder(super.mutate());
|
||||
}
|
||||
|
||||
private final class StrictFirewallHttpHeaders extends HttpHeaders {
|
||||
|
||||
private StrictFirewallHttpHeaders(HttpHeaders delegate) {
|
||||
@@ -783,6 +792,61 @@ public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewal
|
||||
|
||||
}
|
||||
|
||||
private final class StrictFirewallBuilder implements Builder {
|
||||
|
||||
private final Builder delegate;
|
||||
|
||||
private StrictFirewallBuilder(Builder delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder method(HttpMethod httpMethod) {
|
||||
return this.delegate.method(httpMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder uri(URI uri) {
|
||||
return this.delegate.uri(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder path(String path) {
|
||||
return this.delegate.path(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder contextPath(String contextPath) {
|
||||
return this.delegate.contextPath(contextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder header(String headerName, String... headerValues) {
|
||||
return this.delegate.header(headerName, headerValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
return this.delegate.headers(headersConsumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder sslInfo(SslInfo sslInfo) {
|
||||
return this.delegate.sslInfo(sslInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder remoteAddress(InetSocketAddress remoteAddress) {
|
||||
return this.delegate.remoteAddress(remoteAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerHttpRequest build() {
|
||||
return new StrictFirewallHttpRequest(this.delegate.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.web.util;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -49,7 +50,7 @@ public final class UrlUtils {
|
||||
*/
|
||||
public static String buildFullRequestUrl(String scheme, String serverName, int serverPort, String requestURI,
|
||||
String queryString) {
|
||||
scheme = scheme.toLowerCase();
|
||||
scheme = scheme.toLowerCase(Locale.ENGLISH);
|
||||
StringBuilder url = new StringBuilder();
|
||||
url.append(scheme).append("://").append(serverName);
|
||||
// Only add port if not default
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -303,7 +304,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();
|
||||
this.subpath = caseSensitive ? subpath : subpath.toLowerCase(Locale.ROOT);
|
||||
this.length = subpath.length();
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
@@ -311,7 +312,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
|
||||
@Override
|
||||
public boolean matches(String path) {
|
||||
if (!this.caseSensitive) {
|
||||
path = path.toLowerCase();
|
||||
path = path.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return path.startsWith(this.subpath) && (path.length() == this.length || path.charAt(this.length) == '/');
|
||||
}
|
||||
|
||||
+38
-21
@@ -18,6 +18,7 @@ package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -33,16 +34,17 @@ import org.springframework.util.StringUtils;
|
||||
* IPv4 address will never match a request which returns an IPv6 address, and vice-versa.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Steve Riesenberg
|
||||
* @since 3.0.2
|
||||
*/
|
||||
public final class IpAddressMatcher implements RequestMatcher {
|
||||
|
||||
private static Pattern IPV4 = Pattern.compile("\\d{0,3}.\\d{0,3}.\\d{0,3}.\\d{0,3}(/\\d{0,3})?");
|
||||
|
||||
private final int nMaskBits;
|
||||
|
||||
private final InetAddress requiredAddress;
|
||||
|
||||
private final int nMaskBits;
|
||||
|
||||
/**
|
||||
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
|
||||
* 192.168.1.0/24 or 202.24.0.0/14).
|
||||
@@ -50,19 +52,24 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
* come.
|
||||
*/
|
||||
public IpAddressMatcher(String ipAddress) {
|
||||
Assert.hasText(ipAddress, "ipAddress cannot be empty");
|
||||
assertNotHostName(ipAddress);
|
||||
|
||||
String requiredAddress;
|
||||
int nMaskBits;
|
||||
if (ipAddress.indexOf('/') > 0) {
|
||||
String[] addressAndMask = StringUtils.split(ipAddress, "/");
|
||||
ipAddress = addressAndMask[0];
|
||||
this.nMaskBits = Integer.parseInt(addressAndMask[1]);
|
||||
String[] parts = Objects.requireNonNull(StringUtils.split(ipAddress, "/"));
|
||||
requiredAddress = parts[0];
|
||||
nMaskBits = Integer.parseInt(parts[1]);
|
||||
}
|
||||
else {
|
||||
this.nMaskBits = -1;
|
||||
requiredAddress = ipAddress;
|
||||
nMaskBits = -1;
|
||||
}
|
||||
this.requiredAddress = parseAddress(ipAddress);
|
||||
String finalIpAddress = ipAddress;
|
||||
this.requiredAddress = parseAddress(requiredAddress);
|
||||
this.nMaskBits = nMaskBits;
|
||||
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits, () -> String
|
||||
.format("IP address %s is too short for bitmask of length %d", finalIpAddress, this.nMaskBits));
|
||||
.format("IP address %s is too short for bitmask of length %d", requiredAddress, this.nMaskBits));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,9 +77,14 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
return matches(request.getRemoteAddr());
|
||||
}
|
||||
|
||||
public boolean matches(String address) {
|
||||
assertNotHostName(address);
|
||||
InetAddress remoteAddress = parseAddress(address);
|
||||
public boolean matches(String ipAddress) {
|
||||
// Do not match null or blank address
|
||||
if (!StringUtils.hasText(ipAddress)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assertNotHostName(ipAddress);
|
||||
InetAddress remoteAddress = parseAddress(ipAddress);
|
||||
if (!this.requiredAddress.getClass().equals(remoteAddress.getClass())) {
|
||||
return false;
|
||||
}
|
||||
@@ -82,26 +94,31 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
byte[] remAddr = remoteAddress.getAddress();
|
||||
byte[] reqAddr = this.requiredAddress.getAddress();
|
||||
int nMaskFullBytes = this.nMaskBits / 8;
|
||||
byte finalByte = (byte) (0xFF00 >> (this.nMaskBits & 0x07));
|
||||
for (int i = 0; i < nMaskFullBytes; i++) {
|
||||
if (remAddr[i] != reqAddr[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
byte finalByte = (byte) (0xFF00 >> (this.nMaskBits & 0x07));
|
||||
if (finalByte != 0) {
|
||||
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void assertNotHostName(String ipAddress) {
|
||||
boolean isIpv4 = IPV4.matcher(ipAddress).matches();
|
||||
if (isIpv4) {
|
||||
return;
|
||||
}
|
||||
String error = "ipAddress " + ipAddress + " doesn't look like an IP Address. Is it a host name?";
|
||||
Assert.isTrue(ipAddress.charAt(0) == '[' || ipAddress.charAt(0) == ':'
|
||||
|| (Character.digit(ipAddress.charAt(0), 16) != -1 && ipAddress.contains(":")), error);
|
||||
private static void assertNotHostName(String ipAddress) {
|
||||
Assert.isTrue(isIpAddress(ipAddress),
|
||||
() -> String.format("ipAddress %s doesn't look like an IP Address. Is it a host name?", ipAddress));
|
||||
}
|
||||
|
||||
private static boolean isIpAddress(String ipAddress) {
|
||||
// @formatter:off
|
||||
return IPV4.matcher(ipAddress).matches()
|
||||
|| ipAddress.charAt(0) == '['
|
||||
|| ipAddress.charAt(0) == ':'
|
||||
|| Character.digit(ipAddress.charAt(0), 16) != -1
|
||||
&& ipAddress.indexOf(':') > 0;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private InetAddress parseAddress(String address) {
|
||||
|
||||
+43
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,16 +17,20 @@
|
||||
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;
|
||||
@@ -447,6 +451,44 @@ 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));
|
||||
|
||||
+11
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -212,4 +212,14 @@ 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
@@ -513,4 +513,25 @@ class StrictServerWebExchangeFirewallTests {
|
||||
assertThat(exchange.getRequest().getHeaders().get(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenMutateThenHeadersStillFirewalled() {
|
||||
String invalidHeaderName = "bad name";
|
||||
this.firewall.setAllowedHeaderNames((name) -> !name.equals(invalidHeaderName));
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
ServerWebExchange mutatedExchange = exchange.mutate().request(exchange.getRequest().mutate().build()).build();
|
||||
HttpHeaders headers = mutatedExchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class)
|
||||
.isThrownBy(() -> headers.get(invalidHeaderName));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMutatedFirewalledExchangeGetHeaderWhenNotAllowedHeaderNameThenException() {
|
||||
String invalidHeaderName = "bad name";
|
||||
this.firewall.setAllowedHeaderNames((name) -> !name.equals(invalidHeaderName));
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().mutate().build().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class)
|
||||
.isThrownBy(() -> headers.get(invalidHeaderName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -126,4 +126,31 @@ public class IpAddressMatcherTests {
|
||||
.withMessage("ipAddress 123.156.7.18.org doesn't look like an IP Address. Is it a host name?");
|
||||
}
|
||||
|
||||
// gh-15527
|
||||
@Test
|
||||
public void matchesWhenIpAddressIsLoopbackAndAddressIsNullThenFalse() {
|
||||
IpAddressMatcher ipAddressMatcher = new IpAddressMatcher("127.0.0.1");
|
||||
assertThat(ipAddressMatcher.matches((String) null)).isFalse();
|
||||
}
|
||||
|
||||
// gh-15527
|
||||
@Test
|
||||
public void matchesWhenAddressIsNullThenFalse() {
|
||||
assertThat(this.v4matcher.matches((String) null)).isFalse();
|
||||
}
|
||||
|
||||
// gh-15527
|
||||
@Test
|
||||
public void constructorWhenRequiredAddressIsNullThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpAddressMatcher(null))
|
||||
.withMessage("ipAddress cannot be empty");
|
||||
}
|
||||
|
||||
// gh-15527
|
||||
@Test
|
||||
public void constructorWhenRequiredAddressIsEmptyThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpAddressMatcher(""))
|
||||
.withMessage("ipAddress cannot be empty");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user