1
0
mirror of synced 2026-08-07 02:38:47 +00:00

Add OAuth Support for HTTP Interface Client

Closes gh-16858
This commit is contained in:
Rob Winch
2025-05-08 16:21:09 -05:00
parent 502b0b7f95
commit b2325e4176
31 changed files with 1647 additions and 22 deletions
@@ -0,0 +1,58 @@
/*
* Copyright 2002-2025 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.oauth2.client.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* This annotation can be added to the method of an interface based HTTP client created
* using {@link org.springframework.web.service.invoker.HttpServiceProxyFactory} to
* automatically associate an OAuth token with the request.
*
* @author Rob Winch
* @since 7.0
* @see org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClientRegistrationId {
/**
* Sets the client registration identifier.
* @return the client registration identifier
*/
@AliasFor("value")
String registrationId() default "";
/**
* The default attribute for this annotation. This is an alias for
* {@link #registrationId()}. For example,
* {@code @RegisteredOAuth2AuthorizedClient("login-client")} is equivalent to
* {@code @RegisteredOAuth2AuthorizedClient(registrationId="login-client")}.
* @return the client registration identifier
*/
@AliasFor("registrationId")
String value() default "";
}
@@ -0,0 +1,69 @@
/*
* Copyright 2002-2025 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.oauth2.client.web;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.util.Assert;
/**
* Used for accessing the attribute that stores the the
* {@link ClientRegistration#getRegistrationId()}. This ensures that
* {@link org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor}
* aligns with all of ways of setting on both
* {@link org.springframework.web.client.RestClient} and
* {@link org.springframework.web.reactive.function.client.WebClient}.
*
* @see org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor
* @see org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver
* @see org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction
* @see org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction
*/
public final class ClientAttributes {
private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = ClientRegistration.class.getName()
.concat(".CLIENT_REGISTRATION_ID");
/**
* Resolves the {@link ClientRegistration#getRegistrationId() clientRegistrationId} to
* be used to look up the {@link OAuth2AuthorizedClient}.
* @param attributes the to search
* @return the registration id to use.
*/
public static String resolveClientRegistrationId(Map<String, Object> attributes) {
return (String) attributes.get(CLIENT_REGISTRATION_ID_ATTR_NAME);
}
/**
* Produces a Consumer that adds the {@link ClientRegistration#getRegistrationId()
* clientRegistrationId} to be used to look up the {@link OAuth2AuthorizedClient}.
* @param clientRegistrationId the {@link ClientRegistration#getRegistrationId()
* clientRegistrationId} to be used to look up the {@link OAuth2AuthorizedClient}
* @return the {@link Consumer} to populate the attributes
*/
public static Consumer<Map<String, Object>> clientRegistrationId(String clientRegistrationId) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
return (attributes) -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId);
}
private ClientAttributes() {
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client;
import java.lang.reflect.Method;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.security.oauth2.client.web.ClientAttributes;
import org.springframework.web.service.invoker.HttpRequestValues;
/**
* Invokes {@link ClientAttributes#clientRegistrationId(String)} with the value specified
* by {@link ClientRegistrationId} on the request.
*
* @author Rob Winch
* @since 7.0
*/
public final class ClientRegistrationIdProcessor implements HttpRequestValues.Processor {
public static ClientRegistrationIdProcessor DEFAULT_INSTANCE = new ClientRegistrationIdProcessor();
@Override
public void process(Method method, @Nullable Object[] arguments, HttpRequestValues.Builder builder) {
ClientRegistrationId registeredId = AnnotationUtils.findAnnotation(method, ClientRegistrationId.class);
if (registeredId != null) {
String registrationId = registeredId.registrationId();
builder.configureAttributes(ClientAttributes.clientRegistrationId(registrationId));
}
}
private ClientRegistrationIdProcessor() {
}
}
@@ -23,7 +23,7 @@ import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.util.Assert;
import org.springframework.security.oauth2.client.web.ClientAttributes;
/**
* A strategy for resolving a {@code clientRegistrationId} from an intercepted request
@@ -36,13 +36,9 @@ import org.springframework.util.Assert;
public final class RequestAttributeClientRegistrationIdResolver
implements OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver {
private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = RequestAttributeClientRegistrationIdResolver.class
.getName()
.concat(".clientRegistrationId");
@Override
public String resolve(HttpRequest request) {
return (String) request.getAttributes().get(CLIENT_REGISTRATION_ID_ATTR_NAME);
return ClientAttributes.resolveClientRegistrationId(request.getAttributes());
}
/**
@@ -54,8 +50,7 @@ public final class RequestAttributeClientRegistrationIdResolver
* @return the {@link Consumer} to populate the attributes
*/
public static Consumer<Map<String, Object>> clientRegistrationId(String clientRegistrationId) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
return (attributes) -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId);
return ClientAttributes.clientRegistrationId(clientRegistrationId);
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client.support;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.invoker.HttpRequestValues;
/**
* Simplify adding OAuth2 support to interface based rest clients that use
* {@link RestClient}.
*
* It will add {@link OAuth2ClientHttpRequestInterceptor} to the {@link RestClient} and
* {@link ClientRegistrationIdProcessor} to the
* {@link org.springframework.web.service.invoker.HttpServiceProxyFactory}.
*
* @author Rob Winch
* @since 7.0
*/
public final class OAuth2RestClientHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer {
private final HttpRequestValues.Processor processor = ClientRegistrationIdProcessor.DEFAULT_INSTANCE;
private final ClientHttpRequestInterceptor interceptor;
private OAuth2RestClientHttpServiceGroupConfigurer(ClientHttpRequestInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
public void configureGroups(Groups<RestClient.Builder> groups) {
// @formatter:off
groups.forEachClient((group, client) ->
client.requestInterceptor(this.interceptor)
);
groups.forEachProxyFactory((group, factory) ->
factory.httpRequestValuesProcessor(this.processor)
);
// @formatter:on
}
public static OAuth2RestClientHttpServiceGroupConfigurer from(
OAuth2AuthorizedClientManager authorizedClientManager) {
OAuth2ClientHttpRequestInterceptor interceptor = new OAuth2ClientHttpRequestInterceptor(
authorizedClientManager);
return new OAuth2RestClientHttpServiceGroupConfigurer(interceptor);
}
}
@@ -44,6 +44,7 @@ import org.springframework.security.oauth2.client.RemoveAuthorizedClientReactive
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.ClientAttributes;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
@@ -104,13 +105,6 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
*/
private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName();
/**
* The client request attribute name used to locate the
* {@link ClientRegistration#getRegistrationId()}
*/
private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = OAuth2AuthorizedClient.class.getName()
.concat(".CLIENT_REGISTRATION_ID");
/**
* The request attribute name used to locate the
* {@link org.springframework.web.server.ServerWebExchange}.
@@ -292,7 +286,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
* @return the {@link Consumer} to populate the attributes
*/
public static Consumer<Map<String, Object>> clientRegistrationId(String clientRegistrationId) {
return (attributes) -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId);
return ClientAttributes.clientRegistrationId(clientRegistrationId);
}
private static String clientRegistrationId(ClientRequest request) {
@@ -300,7 +294,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
if (authorizedClient != null) {
return authorizedClient.getClientRegistration().getRegistrationId();
}
return (String) request.attributes().get(CLIENT_REGISTRATION_ID_ATTR_NAME);
return ClientAttributes.resolveClientRegistrationId(request.attributes());
}
/**
@@ -50,6 +50,7 @@ import org.springframework.security.oauth2.client.RemoveAuthorizedClientOAuth2Au
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.ClientAttributes;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
@@ -136,9 +137,6 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
*/
private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName();
private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = OAuth2AuthorizedClient.class.getName()
.concat(".CLIENT_REGISTRATION_ID");
private static final String AUTHENTICATION_ATTR_NAME = Authentication.class.getName();
private static final String HTTP_SERVLET_REQUEST_ATTR_NAME = HttpServletRequest.class.getName();
@@ -311,7 +309,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
* @return the {@link Consumer} to populate the attributes
*/
public static Consumer<Map<String, Object>> clientRegistrationId(String clientRegistrationId) {
return (attributes) -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId);
return ClientAttributes.clientRegistrationId(clientRegistrationId);
}
/**
@@ -536,7 +534,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
}
static String getClientRegistrationId(Map<String, Object> attrs) {
return (String) attrs.get(CLIENT_REGISTRATION_ID_ATTR_NAME);
return ClientAttributes.resolveClientRegistrationId(attrs);
}
static Authentication getAuthentication(Map<String, Object> attrs) {
@@ -0,0 +1,92 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.reactive.function.client.support;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
import org.springframework.web.service.invoker.HttpRequestValues;
/**
* Simplify adding OAuth2 support to interface based rest clients that use
* {@link WebClient}.
*
* @author Rob Winch
* @since 7.0
*/
public final class OAuth2WebClientHttpServiceGroupConfigurer implements WebClientHttpServiceGroupConfigurer {
private final HttpRequestValues.Processor processor = ClientRegistrationIdProcessor.DEFAULT_INSTANCE;
private final ExchangeFilterFunction filter;
private OAuth2WebClientHttpServiceGroupConfigurer(ExchangeFilterFunction filter) {
this.filter = filter;
}
@Override
public void configureGroups(Groups<WebClient.Builder> groups) {
// @formatter:off
groups.forEachClient((group, client) ->
client.filter(this.filter)
);
groups.forEachProxyFactory((group, factory) ->
factory.httpRequestValuesProcessor(this.processor)
);
// @formatter:on
}
/**
* Create an instance for Reactive web applications from the provided
* {@link ReactiveOAuth2AuthorizedClientManager}.
*
* It will add {@link ServerOAuth2AuthorizedClientExchangeFilterFunction} to the
* {@link WebClient} and {@link ClientRegistrationIdProcessor} to the
* {@link org.springframework.web.service.invoker.HttpServiceProxyFactory}.
* @param authorizedClientManager the manager to use.
* @return the {@link OAuth2WebClientHttpServiceGroupConfigurer}.
*/
public static OAuth2WebClientHttpServiceGroupConfigurer from(
ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
return new OAuth2WebClientHttpServiceGroupConfigurer(filter);
}
/**
* Create an instance for Servlet based environments from the provided
* {@link OAuth2AuthorizedClientManager}.
*
* It will add {@link ServletOAuth2AuthorizedClientExchangeFilterFunction} to the
* {@link WebClient} and {@link ClientRegistrationIdProcessor} to the
* {@link org.springframework.web.service.invoker.HttpServiceProxyFactory}.
* @param authorizedClientManager the manager to use.
* @return the {@link OAuth2WebClientHttpServiceGroupConfigurer}.
*/
public static OAuth2WebClientHttpServiceGroupConfigurer from(
OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
return new OAuth2WebClientHttpServiceGroupConfigurer(filter);
}
}
@@ -0,0 +1,108 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client;
import java.io.IOException;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.invoker.HttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for integration testing {@link ClientRegistrationIdProcessor} with
* {@link MockWebServer}.
*
* @author Rob Winch
* @since 7.0
*/
abstract class AbstractMockServerClientRegistrationIdProcessorTests {
static final String REGISTRATION_ID = "okta";
private final MockWebServer server = new MockWebServer();
private OAuth2AccessToken accessToken;
protected String baseUrl;
protected OAuth2AuthorizedClient authorizedClient;
@BeforeEach
void setup() throws IOException {
this.server.start();
HttpUrl url = this.server.url("/range/");
this.baseUrl = url.toString();
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
.registrationId(REGISTRATION_ID)
.build();
this.accessToken = TestOAuth2AccessTokens.scopes("read", "write");
this.authorizedClient = new OAuth2AuthorizedClient(clientRegistration, "user", this.accessToken);
}
@AfterEach
void cleanup() throws IOException {
if (this.server != null) {
this.server.shutdown();
}
}
void testWithAdapter(HttpExchangeAdapter adapter) throws InterruptedException {
ClientRegistrationIdProcessor processor = ClientRegistrationIdProcessor.DEFAULT_INSTANCE;
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder()
.exchangeAdapter(adapter)
.httpRequestValuesProcessor(processor)
.build();
MessageClient messages = factory.createClient(MessageClient.class);
this.server.enqueue(new MockResponse().setBody("Hello OAuth2!").setResponseCode(200));
assertThat(messages.getMessage()).isEqualTo("Hello OAuth2!");
String authorizationHeader = this.server.takeRequest().getHeader(HttpHeaders.AUTHORIZATION);
assertOAuthTokenValue(authorizationHeader, this.accessToken);
}
private static void assertOAuthTokenValue(String value, OAuth2AccessToken accessToken) {
String tokenType = accessToken.getTokenType().getValue();
String tokenValue = accessToken.getTokenValue();
assertThat(value).isEqualTo("%s %s".formatted(tokenType, tokenValue));
}
interface MessageClient {
@GetExchange("/message")
@ClientRegistrationId(REGISTRATION_ID)
String getMessage();
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Runs tests of {@link ClientRegistrationIdProcessor} with {@link RestClient} to ensure
* that all the parts work together properly.
*
* @author Rob Winch
* @since 7.0
*/
@ExtendWith(MockitoExtension.class)
class ClientRegistrationIdProcessorRestClientTests extends AbstractMockServerClientRegistrationIdProcessorTests {
@Mock
private OAuth2AuthorizedClientManager authorizedClientManager;
@Test
void clientRegistrationIdProcessorWorksWithRestClientAdapter() throws InterruptedException {
OAuth2ClientHttpRequestInterceptor interceptor = new OAuth2ClientHttpRequestInterceptor(
this.authorizedClientManager);
RestClient.Builder builder = RestClient.builder().requestInterceptor(interceptor).baseUrl(this.baseUrl);
ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequest = ArgumentCaptor.forClass(OAuth2AuthorizeRequest.class);
given(this.authorizedClientManager.authorize(authorizeRequest.capture())).willReturn(authorizedClient);
testWithAdapter(RestClientAdapter.create(builder.build()));
assertThat(authorizeRequest.getValue().getClientRegistrationId()).isEqualTo(REGISTRATION_ID);
}
}
@@ -0,0 +1,94 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.security.oauth2.client.web.ClientAttributes;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.service.invoker.HttpRequestValues;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ClientRegistrationIdProcessor}.
*
* @author Rob Winch
* @since 7.0
* @see ClientRegistrationIdProcessorWebClientTests
* @see ClientRegistrationIdProcessorRestClientTests
*/
class ClientRegistrationIdProcessorTests {
ClientRegistrationIdProcessor processor = ClientRegistrationIdProcessor.DEFAULT_INSTANCE;
@Test
void processWhenClientRegistrationIdPresentThenSet() {
HttpRequestValues.Builder builder = HttpRequestValues.builder();
Method hasClientRegistrationId = ReflectionUtils.findMethod(RestService.class, "hasClientRegistrationId");
this.processor.process(hasClientRegistrationId, null, builder);
String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes());
assertThat(registrationId).isEqualTo(RestService.REGISTRATION_ID);
}
@Test
void processWhenMetaClientRegistrationIdPresentThenSet() {
HttpRequestValues.Builder builder = HttpRequestValues.builder();
Method hasClientRegistrationId = ReflectionUtils.findMethod(RestService.class, "hasMetaClientRegistrationId");
this.processor.process(hasClientRegistrationId, null, builder);
String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes());
assertThat(registrationId).isEqualTo(RestService.REGISTRATION_ID);
}
@Test
void processWhenNoClientRegistrationIdPresentThenNull() {
HttpRequestValues.Builder builder = HttpRequestValues.builder();
Method hasClientRegistrationId = ReflectionUtils.findMethod(RestService.class, "noClientRegistrationId");
this.processor.process(hasClientRegistrationId, null, builder);
String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes());
assertThat(registrationId).isNull();
}
interface RestService {
String REGISTRATION_ID = "registrationId";
@ClientRegistrationId(REGISTRATION_ID)
void hasClientRegistrationId();
@MetaClientRegistrationId
void hasMetaClientRegistrationId();
void noClientRegistrationId();
}
@Retention(RetentionPolicy.RUNTIME)
@ClientRegistrationId(RestService.REGISTRATION_ID)
@interface MetaClientRegistrationId {
}
}
@@ -0,0 +1,82 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Runs tests for {@link ClientRegistrationIdProcessor} with {@link WebClient} to ensure
* that all the parts work together properly.
*
* @author Rob Winch
* @since 7.0
*/
@ExtendWith(MockitoExtension.class)
class ClientRegistrationIdProcessorWebClientTests extends AbstractMockServerClientRegistrationIdProcessorTests {
@Test
void clientRegistrationIdProcessorWorksWithReactiveWebClient() throws InterruptedException {
ReactiveOAuth2AuthorizedClientManager authorizedClientManager = mock(
ReactiveOAuth2AuthorizedClientManager.class);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
WebClient.Builder builder = WebClient.builder().filter(oauth2Client).baseUrl(this.baseUrl);
ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequest = ArgumentCaptor.forClass(OAuth2AuthorizeRequest.class);
given(authorizedClientManager.authorize(authorizeRequest.capture()))
.willReturn(Mono.just(this.authorizedClient));
testWithAdapter(WebClientAdapter.create(builder.build()));
assertThat(authorizeRequest.getValue().getClientRegistrationId()).isEqualTo(REGISTRATION_ID);
}
@Test
void clientRegistrationIdProcessorWorksWithServletWebClient() throws InterruptedException {
OAuth2AuthorizedClientManager authorizedClientManager = mock(OAuth2AuthorizedClientManager.class);
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
WebClient.Builder builder = WebClient.builder().filter(oauth2Client).baseUrl(this.baseUrl);
ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequest = ArgumentCaptor.forClass(OAuth2AuthorizeRequest.class);
given(authorizedClientManager.authorize(authorizeRequest.capture())).willReturn(this.authorizedClient);
testWithAdapter(WebClientAdapter.create(builder.build()));
assertThat(authorizeRequest.getValue().getClientRegistrationId()).isEqualTo(REGISTRATION_ID);
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.client.support;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.registry.HttpServiceGroupConfigurer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
/**
* Tests {@link OAuth2RestClientHttpServiceGroupConfigurer}.
*
* @author Rob Winch
*/
@ExtendWith(MockitoExtension.class)
class OAuth2RestClientHttpServiceGroupConfigurerTests {
@Mock
private OAuth2AuthorizedClientManager authoriedClientManager;
@Mock
private HttpServiceGroupConfigurer.Groups<RestClient.Builder> groups;
@Captor
ArgumentCaptor<HttpServiceGroupConfigurer.ProxyFactoryCallback> forProxyFactory;
@Mock
private HttpServiceProxyFactory.Builder factoryBuilder;
@Captor
private ArgumentCaptor<HttpServiceGroupConfigurer.ClientCallback<RestClient.Builder>> configureClient;
@Mock
private RestClient.Builder clientBuilder;
@Test
void configureGroupsConfigureProxyFactory() {
OAuth2RestClientHttpServiceGroupConfigurer configurer = OAuth2RestClientHttpServiceGroupConfigurer
.from(this.authoriedClientManager);
configurer.configureGroups(this.groups);
verify(this.groups).forEachProxyFactory(this.forProxyFactory.capture());
this.forProxyFactory.getValue().withProxyFactory(null, this.factoryBuilder);
verify(this.factoryBuilder).httpRequestValuesProcessor(ClientRegistrationIdProcessor.DEFAULT_INSTANCE);
}
@Test
void configureGroupsConfigureClient() {
OAuth2RestClientHttpServiceGroupConfigurer configurer = OAuth2RestClientHttpServiceGroupConfigurer
.from(this.authoriedClientManager);
configurer.configureGroups(this.groups);
verify(this.groups).forEachClient(this.configureClient.capture());
this.configureClient.getValue().withClient(null, this.clientBuilder);
verify(this.clientBuilder).requestInterceptor(any(OAuth2ClientHttpRequestInterceptor.class));
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2002-2025 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.oauth2.client.web.reactive.function.client.support;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.registry.HttpServiceGroupConfigurer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
/**
* Tests {@link OAuth2WebClientHttpServiceGroupConfigurer}.
*
* @author Rob Winch
*/
@ExtendWith(MockitoExtension.class)
class OAuth2WebClientHttpServiceGroupConfigurerTests {
@Mock
private OAuth2AuthorizedClientManager authoriedClientManager;
@Mock
private HttpServiceGroupConfigurer.Groups<WebClient.Builder> groups;
@Captor
ArgumentCaptor<HttpServiceGroupConfigurer.ProxyFactoryCallback> forProxyFactory;
@Mock
private HttpServiceProxyFactory.Builder factoryBuilder;
@Captor
private ArgumentCaptor<HttpServiceGroupConfigurer.ClientCallback<WebClient.Builder>> configureClient;
@Mock
private WebClient.Builder clientBuilder;
@Test
void configureGroupsConfigureProxyFactory() {
OAuth2WebClientHttpServiceGroupConfigurer configurer = OAuth2WebClientHttpServiceGroupConfigurer
.from(this.authoriedClientManager);
configurer.configureGroups(this.groups);
verify(this.groups).forEachProxyFactory(this.forProxyFactory.capture());
this.forProxyFactory.getValue().withProxyFactory(null, this.factoryBuilder);
verify(this.factoryBuilder).httpRequestValuesProcessor(ClientRegistrationIdProcessor.DEFAULT_INSTANCE);
}
@Test
void configureGroupsConfigureClient() {
OAuth2WebClientHttpServiceGroupConfigurer configurer = OAuth2WebClientHttpServiceGroupConfigurer
.from(this.authoriedClientManager);
configurer.configureGroups(this.groups);
verify(this.groups).forEachClient(this.configureClient.capture());
this.configureClient.getValue().withClient(null, this.clientBuilder);
verify(this.clientBuilder).filter(any(ExchangeFilterFunction.class));
}
}