mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-28 06:42:49 +00:00
Merge branch '6.3.x'
This commit is contained in:
commit
4dd00fe146
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,16 +16,17 @@
|
|||||||
|
|
||||||
package org.springframework.security.config.web.server;
|
package org.springframework.security.config.web.server;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.util.Collections;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
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.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
@ -61,6 +62,9 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
|||||||
|
|
||||||
private final ServerLogoutHandler logoutHandler;
|
private final ServerLogoutHandler logoutHandler;
|
||||||
|
|
||||||
|
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
|
||||||
|
new OAuth2ErrorEncoder());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
||||||
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
|
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
|
||||||
@ -85,7 +89,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
|||||||
if (ex instanceof AuthenticationServiceException) {
|
if (ex instanceof AuthenticationServiceException) {
|
||||||
return Mono.error(ex);
|
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()))
|
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||||
.flatMap(this.authenticationManager::authenticate)
|
.flatMap(this.authenticationManager::authenticate)
|
||||||
@ -94,7 +98,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
|||||||
if (ex instanceof AuthenticationServiceException) {
|
if (ex instanceof AuthenticationServiceException) {
|
||||||
return Mono.error(ex);
|
return Mono.error(ex);
|
||||||
}
|
}
|
||||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||||
})
|
})
|
||||||
.flatMap((authentication) -> {
|
.flatMap((authentication) -> {
|
||||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||||
@ -102,19 +106,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);
|
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
OAuth2Error error = oauth2Error(ex);
|
return this.errorHttpMessageConverter.write(Mono.just(oauth2Error(ex)), ResolvableType.forClass(Object.class),
|
||||||
byte[] bytes = String.format("""
|
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
|
||||||
{
|
exchange.getResponse(), Collections.emptyMap());
|
||||||
"error_code": "%s",
|
|
||||||
"error_description": "%s",
|
|
||||||
"error_uri: "%s"
|
|
||||||
}
|
|
||||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
|
||||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
|
||||||
return response.writeWith(Flux.just(buffer));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private OAuth2Error oauth2Error(Exception ex) {
|
private OAuth2Error oauth2Error(Exception ex) {
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package org.springframework.security.config.web.server;
|
package org.springframework.security.config.web.server;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@ -25,14 +25,15 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
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.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.ServerHttpRequest;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
|
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
|
||||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||||
@ -44,6 +45,7 @@ import org.springframework.util.LinkedMultiValueMap;
|
|||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.reactive.function.BodyInserters;
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
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.UriComponents;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
@ -63,6 +65,9 @@ public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHan
|
|||||||
|
|
||||||
private final ReactiveOidcSessionRegistry sessionRegistry;
|
private final ReactiveOidcSessionRegistry sessionRegistry;
|
||||||
|
|
||||||
|
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
|
||||||
|
new OAuth2ErrorEncoder());
|
||||||
|
|
||||||
private WebClient web = WebClient.create();
|
private WebClient web = WebClient.create();
|
||||||
|
|
||||||
private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
|
private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
|
||||||
@ -101,7 +106,7 @@ public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHan
|
|||||||
totalCount.intValue()));
|
totalCount.intValue()));
|
||||||
}
|
}
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
|
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
@ -164,17 +169,11 @@ public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHan
|
|||||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
|
private Mono<Void> handleLogoutFailure(ServerWebExchange exchange, OAuth2Error error) {
|
||||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
byte[] bytes = String.format("""
|
return this.errorHttpMessageConverter.write(Mono.just(error), ResolvableType.forClass(Object.class),
|
||||||
{
|
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
|
||||||
"error_code": "%s",
|
exchange.getResponse(), Collections.emptyMap());
|
||||||
"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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,6 +51,7 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||||
@ -101,6 +102,7 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.hasValue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.atLeastOnce;
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
@ -199,7 +201,10 @@ public class OidcLogoutSpecTests {
|
|||||||
.body(BodyInserters.fromFormData("logout_token", "invalid"))
|
.body(BodyInserters.fromFormData("logout_token", "invalid"))
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus()
|
.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();
|
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,9 +271,10 @@ public class OidcLogoutSpecTests {
|
|||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus()
|
.expectStatus()
|
||||||
.isBadRequest()
|
.isBadRequest()
|
||||||
.expectBody(String.class)
|
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||||
.value(containsString("partial_logout"))
|
})
|
||||||
.value(containsString("not all sessions were terminated"));
|
.value(hasValue("partial_logout"))
|
||||||
|
.value(hasValue(containsString("not all sessions were terminated")));
|
||||||
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user