1
0
mirror of synced 2026-05-22 21:33:16 +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
+2
View File
@@ -19,6 +19,8 @@
*** xref:features/exploits/headers.adoc[HTTP Headers]
*** xref:features/exploits/http.adoc[HTTP Requests]
** xref:features/integrations/index.adoc[Integrations]
*** REST Client
**** xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
*** xref:features/integrations/cryptography.adoc[Cryptography]
*** xref:features/integrations/data.adoc[Spring Data]
*** xref:features/integrations/concurrency.adoc[Java's Concurrency APIs]
@@ -0,0 +1,66 @@
= HTTP Interface Integration
Spring Security's OAuth Support can integrate with `RestClient` and `WebClient` {spring-framework-reference-url}/integration/rest-clients.html[HTTP Interface based REST Clients].
[[configuration]]
== Configuration
After xref:features/integrations/rest/http-interface.adoc#configuration-restclient[RestClient] or xref:features/integrations/rest/http-interface.adoc#configuration-webclient[WebClient] specific configuration, usage of xref:features/integrations/rest/http-interface.adoc[] only requires adding a xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] to methods that require OAuth.
Since the presense of xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] determines if and how the OAuth token will be resolved, it is safe to add Spring Security's OAuth support any configuration.
[[configuration-restclient]]
=== RestClient Configuration
Spring Security's OAuth Support can integrate with {spring-framework-reference-url}/integration/rest-clients.html[HTTP Interface based REST Clients] backed by RestClient.
The first step is to xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[create an `OAuthAuthorizedClientManager` Bean].
Next you must configure `HttpServiceProxyFactory` and `RestClient` to be aware of xref:./http-interface.adoc#client-registration-id[@ClientRegistrationId]
To simplify this configuration, use javadoc:org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer[].
include-code::./RestClientHttpInterfaceIntegrationConfiguration[tag=config,indent=0]
The configuration:
- Adds xref:features/integrations/rest/http-interface.adoc#client-registration-id-processor[`ClientRegistrationIdProcessor`] to {spring-framework-reference-url}/integration/rest-clients.html#rest-http-interface[`HttpServiceProxyFactory`]
- Adds xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-rest-client[`OAuth2ClientHttpRequestInterceptor`] to the `RestClient`
[[configuration-webclient]]
=== WebClient Configuration
Spring Security's OAuth Support can integrate with {spring-framework-reference-url}/integration/rest-clients.html[HTTP Interface based REST Clients] backed by `WebClient`.
The first step is to xref:reactive/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[create an `ReactiveOAuthAuthorizedClientManager` Bean].
Next you must configure `HttpServiceProxyFactory` and `WebRestClient` to be aware of xref:./http-interface.adoc#client-registration-id[@ClientRegistrationId]
To simplify this configuration, use javadoc:org.springframework.security.oauth2.client.web.reactive.function.client.support.OAuth2WebClientHttpServiceGroupConfigurer[].
include-code::./ServerWebClientHttpInterfaceIntegrationConfiguration[tag=config,indent=0]
The configuration:
- Adds xref:features/integrations/rest/http-interface.adoc#client-registration-id-processor[`ClientRegistrationIdProcessor`] to {spring-framework-reference-url}/integration/rest-clients.html#rest-http-interface[`HttpServiceProxyFactory`]
- Adds xref:reactive/oauth2/client/authorized-clients.adoc#oauth2-client-web-client[`ServerOAuth2AuthorizedClientExchangeFilterFunction`] to the `WebClient`
[[client-registration-id]]
== @ClientRegistrationId
You can add the javadoc:org.springframework.security.oauth2.client.annotation.ClientRegistrationId[] on the HTTP Interface to specify which javadoc:org.springframework.security.oauth2.client.registration.ClientRegistration[] to use.
include-code::./UserService[tag=getAuthenticatedUser]
The xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] will be processed by xref:features/integrations/rest/http-interface.adoc#client-registration-id-processor[`ClientRegistrationIdProcessor`]
[[client-registration-id-processor]]
== `ClientRegistrationIdProcessor`
The xref:features/integrations/rest/http-interface.adoc#configuration[configured] javadoc:org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor[] will:
- Automatically invoke javadoc:org.springframework.security.oauth2.client.web.ClientAttributes#clientRegistrationId(java.lang.String)[] for each xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`].
- This adds the javadoc:org.springframework.security.oauth2.client.registration.ClientRegistration#getId()[] to the attributes
The `id` is then processed by:
- `OAuth2ClientHttpRequestInterceptor` for xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-rest-client[RestClient Integration]
- xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-web-client[`ServletOAuth2AuthorizedClientExchangeFilterFunction`] (servlets) or xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-web-client[`ServerOAuth2AuthorizedClientExchangeFilterFunction`] (reactive environments) for `WebClient`.
@@ -495,6 +495,11 @@ class RestClientConfig {
----
=====
[[oauth2-client-rest-client-interface]]
=== HTTP Interface Integration
Spring Security's OAuth support integrates with xref:features/integrations/rest/http-interface.adoc[].
[[oauth2-client-web-client]]
== [[oauth2Client-webclient-servlet]]WebClient Integration for Servlet Environments
+1
View File
@@ -7,3 +7,4 @@ Below are the highlights of the release, or you can view https://github.com/spri
== Web
* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
+3
View File
@@ -39,6 +39,8 @@ dependencies {
testImplementation project(':spring-security-config')
testImplementation project(path : ':spring-security-config', configuration : 'tests')
testImplementation project(':spring-security-test')
testImplementation project(':spring-security-oauth2-client')
testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation 'com.unboundid:unboundid-ldapsdk'
testImplementation libs.webauthn4j.core
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
@@ -49,6 +51,7 @@ dependencies {
testImplementation 'org.springframework:spring-webmvc'
testImplementation 'jakarta.servlet:jakarta.servlet-api'
testImplementation 'io.mockk:mockk'
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
testImplementation "org.junit.jupiter:junit-jupiter-engine"
@@ -0,0 +1,28 @@
/*
* 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 clients 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.docs.features.integrations.rest.clientregistrationid;
/**
* A user.
* @param login
* @param id
* @param name
* @author Rob Winch
* @see UserService
*/
public record User(String login, int id, String name) {
}
@@ -0,0 +1,36 @@
/*
* 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 clients 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.docs.features.integrations.rest.clientregistrationid;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
/**
* Demonstrates a service for {@link ClientRegistrationId} and HTTP Interface clients.
* @author Rob Winch
*/
@HttpExchange
public interface UserService {
// tag::getAuthenticatedUser[]
@GetExchange("/user")
@ClientRegistrationId("github")
User getAuthenticatedUser();
// end::getAuthenticatedUser[]
}
@@ -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 clients 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.docs.features.integrations.rest.configurationrestclient;
import okhttp3.mockwebserver.MockWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.ImportHttpServices;
import static org.mockito.Mockito.mock;
/**
* Documentation for {@link OAuth2RestClientHttpServiceGroupConfigurer}.
* @author Rob Winch
*/
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(types = UserService.class)
public class RestClientHttpInterfaceIntegrationConfiguration {
// tag::config[]
@Bean
OAuth2RestClientHttpServiceGroupConfigurer securityConfigurer(
OAuth2AuthorizedClientManager manager) {
return OAuth2RestClientHttpServiceGroupConfigurer.from(manager);
}
// end::config[]
@Bean
OAuth2AuthorizedClientManager authorizedClientManager() {
return mock(OAuth2AuthorizedClientManager.class);
}
@Bean
RestClientHttpServiceGroupConfigurer groupConfigurer(MockWebServer server) {
return groups -> {
groups
.forEachClient((group, builder) -> builder
.baseUrl(server.url("").toString())
.defaultHeader("Accept", "application/vnd.github.v3+json"));
};
}
@Bean
MockWebServer mockServer() {
return new MockWebServer();
}
}
@@ -0,0 +1,73 @@
/*
* 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 clients 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.docs.features.integrations.rest.configurationrestclient;
import java.time.Duration;
import java.time.Instant;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
/**
* Tests RestClient configuration for HTTP Interface clients.
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RestClientHttpInterfaceIntegrationConfiguration.class)
class RestClientHttpInterfaceIntegrationConfigurationTests {
@Test
void getAuthenticatedUser(@Autowired MockWebServer webServer, @Autowired OAuth2AuthorizedClientManager authorizedClients, @Autowired UserService users)
throws InterruptedException {
ClientRegistration registration = CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("github").build();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(5));
OAuth2AccessToken token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "1234",
issuedAt, expiresAt);
OAuth2AuthorizedClient result = new OAuth2AuthorizedClient(registration, "rob", token);
given(authorizedClients.authorize(any())).willReturn(result);
webServer.enqueue(new MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(
"""
{"login": "rob_winch", "id": 1234, "name": "Rob Winch" }
"""));
users.getAuthenticatedUser();
assertThat(webServer.takeRequest().getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + token.getTokenValue());
}
}
@@ -0,0 +1,75 @@
/*
* 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 clients 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.docs.features.integrations.rest.configurationwebclient;
import java.time.Duration;
import java.time.Instant;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
/**
* Demonstrates configuring RestClient with interface based proxy clients.
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ServerWebClientHttpInterfaceIntegrationConfiguration.class)
class ServerRestClientHttpInterfaceIntegrationConfigurationTests {
@Test
void getAuthenticatedUser(@Autowired MockWebServer webServer, @Autowired ReactiveOAuth2AuthorizedClientManager authorizedClients, @Autowired UserService users)
throws InterruptedException {
ClientRegistration registration = CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("github").build();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(5));
OAuth2AccessToken token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "1234",
issuedAt, expiresAt);
OAuth2AuthorizedClient result = new OAuth2AuthorizedClient(registration, "rob", token);
given(authorizedClients.authorize(any())).willReturn(Mono.just(result));
webServer.enqueue(new MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(
"""
{"login": "rob_winch", "id": 1234, "name": "Rob Winch" }
"""));
users.getAuthenticatedUser();
assertThat(webServer.takeRequest().getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + token.getTokenValue());
}
}
@@ -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 clients 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.docs.features.integrations.rest.configurationwebclient;
import okhttp3.mockwebserver.MockWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer;
import org.springframework.security.oauth2.client.web.reactive.function.client.support.OAuth2WebClientHttpServiceGroupConfigurer;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.HttpServiceGroup;
import org.springframework.web.service.registry.ImportHttpServices;
import static org.mockito.Mockito.mock;
/**
* Documentation for {@link OAuth2RestClientHttpServiceGroupConfigurer}.
* @author Rob Winch
*/
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(types = UserService.class, clientType = HttpServiceGroup.ClientType.WEB_CLIENT)
public class ServerWebClientHttpInterfaceIntegrationConfiguration {
// tag::config[]
@Bean
OAuth2WebClientHttpServiceGroupConfigurer securityConfigurer(
ReactiveOAuth2AuthorizedClientManager manager) {
return OAuth2WebClientHttpServiceGroupConfigurer.from(manager);
}
// end::config[]
@Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager() {
return mock(ReactiveOAuth2AuthorizedClientManager.class);
}
@Bean
WebClientHttpServiceGroupConfigurer groupConfigurer(MockWebServer server) {
return groups -> {
String baseUrl = server.url("").toString();
groups
.forEachClient((group, builder) -> builder
.baseUrl(baseUrl)
.defaultHeader("Accept", "application/vnd.github.v3+json"));
};
}
@Bean
MockWebServer mockServer() {
return new MockWebServer();
}
}
@@ -0,0 +1,29 @@
/*
* 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 clients 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.kt.docs.features.integrations.rest.clientregistrationid
/**
* A user.
* @param login
* @param id
* @param name
* @author Rob Winch
* @see UserService
*/
@JvmRecord
data class User(val login: String, val id: Int, val name: String)
@@ -0,0 +1,35 @@
/*
* 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 clients 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.kt.docs.features.integrations.rest.clientregistrationid
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId
import org.springframework.web.service.annotation.GetExchange
import org.springframework.web.service.annotation.HttpExchange
/**
* Demonstrates a service for {@link ClientRegistrationId} and HTTP Interface clients.
* @author Rob Winch
*/
@HttpExchange
interface UserService {
// tag::getAuthenticatedUser[]
@GetExchange("/user")
@ClientRegistrationId("github")
fun getAuthenticatedUser() : User
// end::getAuthenticatedUser[]
}
@@ -0,0 +1,65 @@
/*
* 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 clients 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.kt.docs.features.integrations.rest.configurationrestclient
import okhttp3.mockwebserver.MockWebServer
import org.mockito.Mockito
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.kt.docs.features.integrations.rest.clientregistrationid.UserService
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager
import org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer
import org.springframework.web.client.RestClient
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer
import org.springframework.web.service.registry.HttpServiceGroup
import org.springframework.web.service.registry.HttpServiceGroupConfigurer
import org.springframework.web.service.registry.HttpServiceGroupConfigurer.ClientCallback
import org.springframework.web.service.registry.ImportHttpServices
/**
* Documentation for [OAuth2RestClientHttpServiceGroupConfigurer].
* @author Rob Winch
*/
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(types = [UserService::class])
class RestClientHttpInterfaceIntegrationConfiguration {
// tag::config[]
@Bean
fun securityConfigurer(manager: OAuth2AuthorizedClientManager): OAuth2RestClientHttpServiceGroupConfigurer {
return OAuth2RestClientHttpServiceGroupConfigurer.from(manager)
}
// end::config[]
@Bean
fun authorizedClientManager(): OAuth2AuthorizedClientManager? {
return Mockito.mock<OAuth2AuthorizedClientManager?>(OAuth2AuthorizedClientManager::class.java)
}
@Bean
fun groupConfigurer(server: MockWebServer): RestClientHttpServiceGroupConfigurer {
return RestClientHttpServiceGroupConfigurer { groups: HttpServiceGroupConfigurer.Groups<RestClient.Builder> ->
groups.forEachClient(ClientCallback { group: HttpServiceGroup, builder: RestClient.Builder ->
builder
.baseUrl(server.url("").toString())
})
}
}
@Bean
fun mockServer(): MockWebServer {
return MockWebServer()
}
}
@@ -0,0 +1,75 @@
/*
* 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 clients 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.kt.docs.features.integrations.rest.configurationrestclient
import io.mockk.every
import io.mockk.mockkObject
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider
import org.springframework.security.kt.docs.features.integrations.rest.clientregistrationid.UserService
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager
import org.springframework.security.oauth2.core.OAuth2AccessToken
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension
import java.time.Duration
import java.time.Instant
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [RestClientHttpInterfaceIntegrationConfiguration::class])
internal class RestClientHttpInterfaceIntegrationConfigurationTests {
@Test
fun getAuthenticatedUser(
@Autowired webServer: MockWebServer,
@Autowired authorizedClients: OAuth2AuthorizedClientManager,
@Autowired users: UserService
) {
val registration = CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("github").build()
val issuedAt = Instant.now()
val expiresAt = issuedAt.plus(Duration.ofMinutes(5))
val token = OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "1234",
issuedAt, expiresAt
)
val result = OAuth2AuthorizedClient(registration, "rob", token)
mockkObject(authorizedClients)
every {
authorizedClients.authorize(any())
} returns result
webServer.enqueue(
MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(
"""
{"login": "rob_winch", "id": 1234, "name": "Rob Winch" }
""".trimIndent()
)
)
users.getAuthenticatedUser()
Assertions.assertThat(webServer.takeRequest().getHeader(HttpHeaders.AUTHORIZATION))
.isEqualTo("Bearer " + token.getTokenValue())
}
}
@@ -0,0 +1,78 @@
/*
* 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 clients 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.kt.docs.features.integrations.rest.configurationwebclient
import io.mockk.every
import io.mockk.mockkObject
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider
import org.springframework.security.kt.docs.features.integrations.rest.clientregistrationid.UserService
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager
import org.springframework.security.oauth2.core.OAuth2AccessToken
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension
import reactor.core.publisher.Mono
import java.time.Duration
import java.time.Instant
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [ServerWebClientHttpInterfaceIntegrationConfiguration::class])
internal class ServerRestClientHttpInterfaceIntegrationConfigurationTests {
@Test
@Throws(InterruptedException::class)
fun getAuthenticatedUser(
@Autowired webServer: MockWebServer,
@Autowired authorizedClients: ReactiveOAuth2AuthorizedClientManager,
@Autowired users: UserService
) {
val registration = CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("github").build()
val issuedAt = Instant.now()
val expiresAt = issuedAt.plus(Duration.ofMinutes(5))
val token = OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "1234",
issuedAt, expiresAt
)
val result = OAuth2AuthorizedClient(registration, "rob", token)
mockkObject(authorizedClients)
every {
authorizedClients.authorize(any())
} returns Mono.just(result)
webServer.enqueue(
MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(
"""
{"login": "rob_winch", "id": 1234, "name": "Rob Winch" }
""".trimIndent()
)
)
users.getAuthenticatedUser()
Assertions.assertThat(webServer.takeRequest().getHeader(HttpHeaders.AUTHORIZATION))
.isEqualTo("Bearer " + token.getTokenValue())
}
}
@@ -0,0 +1,72 @@
/*
* 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 clients 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.kt.docs.features.integrations.rest.configurationwebclient
import okhttp3.mockwebserver.MockWebServer
import org.mockito.Mockito
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.kt.docs.features.integrations.rest.clientregistrationid.UserService
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager
import org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer
import org.springframework.security.oauth2.client.web.reactive.function.client.support.OAuth2WebClientHttpServiceGroupConfigurer
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer
import org.springframework.web.service.registry.HttpServiceGroup
import org.springframework.web.service.registry.HttpServiceGroupConfigurer
import org.springframework.web.service.registry.HttpServiceGroupConfigurer.ClientCallback
import org.springframework.web.service.registry.ImportHttpServices
/**
* Documentation for [OAuth2RestClientHttpServiceGroupConfigurer].
* @author Rob Winch
*/
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(types = [UserService::class], clientType = HttpServiceGroup.ClientType.WEB_CLIENT)
class ServerWebClientHttpInterfaceIntegrationConfiguration {
// tag::config[]
@Bean
fun securityConfigurer(
manager: ReactiveOAuth2AuthorizedClientManager?
): OAuth2WebClientHttpServiceGroupConfigurer {
return OAuth2WebClientHttpServiceGroupConfigurer.from(manager)
}
// end::config[]
@Bean
fun authorizedClientManager(): ReactiveOAuth2AuthorizedClientManager? {
return Mockito.mock<ReactiveOAuth2AuthorizedClientManager?>(ReactiveOAuth2AuthorizedClientManager::class.java)
}
@Bean
fun groupConfigurer(server: MockWebServer): WebClientHttpServiceGroupConfigurer {
return WebClientHttpServiceGroupConfigurer { groups: HttpServiceGroupConfigurer.Groups<WebClient.Builder?>? ->
val baseUrl = server.url("").toString()
groups!!
.forEachClient(ClientCallback { group: HttpServiceGroup?, builder: WebClient.Builder? ->
builder!!
.baseUrl(baseUrl)
.defaultHeader("Accept", "application/vnd.github.v3+json")
})
}
}
@Bean
fun mockServer(): MockWebServer {
return MockWebServer()
}
}