Skip to content

Commit b2325e4

Browse files
committed
Add OAuth Support for HTTP Interface Client
Closes gh-16858
1 parent 502b0b7 commit b2325e4

File tree

31 files changed

+1647
-22
lines changed

31 files changed

+1647
-22
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
*** xref:features/exploits/headers.adoc[HTTP Headers]
2020
*** xref:features/exploits/http.adoc[HTTP Requests]
2121
** xref:features/integrations/index.adoc[Integrations]
22+
*** REST Client
23+
**** xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
2224
*** xref:features/integrations/cryptography.adoc[Cryptography]
2325
*** xref:features/integrations/data.adoc[Spring Data]
2426
*** xref:features/integrations/concurrency.adoc[Java's Concurrency APIs]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
= HTTP Interface Integration
2+
3+
Spring Security's OAuth Support can integrate with `RestClient` and `WebClient` {spring-framework-reference-url}/integration/rest-clients.html[HTTP Interface based REST Clients].
4+
5+
6+
[[configuration]]
7+
== Configuration
8+
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.
9+
10+
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.
11+
12+
[[configuration-restclient]]
13+
=== RestClient Configuration
14+
15+
Spring Security's OAuth Support can integrate with {spring-framework-reference-url}/integration/rest-clients.html[HTTP Interface based REST Clients] backed by RestClient.
16+
The first step is to xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[create an `OAuthAuthorizedClientManager` Bean].
17+
18+
Next you must configure `HttpServiceProxyFactory` and `RestClient` to be aware of xref:./http-interface.adoc#client-registration-id[@ClientRegistrationId]
19+
To simplify this configuration, use javadoc:org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer[].
20+
21+
include-code::./RestClientHttpInterfaceIntegrationConfiguration[tag=config,indent=0]
22+
23+
The configuration:
24+
25+
- 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`]
26+
- Adds xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-rest-client[`OAuth2ClientHttpRequestInterceptor`] to the `RestClient`
27+
28+
[[configuration-webclient]]
29+
=== WebClient Configuration
30+
31+
Spring Security's OAuth Support can integrate with {spring-framework-reference-url}/integration/rest-clients.html[HTTP Interface based REST Clients] backed by `WebClient`.
32+
The first step is to xref:reactive/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[create an `ReactiveOAuthAuthorizedClientManager` Bean].
33+
34+
Next you must configure `HttpServiceProxyFactory` and `WebRestClient` to be aware of xref:./http-interface.adoc#client-registration-id[@ClientRegistrationId]
35+
To simplify this configuration, use javadoc:org.springframework.security.oauth2.client.web.reactive.function.client.support.OAuth2WebClientHttpServiceGroupConfigurer[].
36+
37+
include-code::./ServerWebClientHttpInterfaceIntegrationConfiguration[tag=config,indent=0]
38+
39+
The configuration:
40+
41+
- 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`]
42+
- Adds xref:reactive/oauth2/client/authorized-clients.adoc#oauth2-client-web-client[`ServerOAuth2AuthorizedClientExchangeFilterFunction`] to the `WebClient`
43+
44+
45+
[[client-registration-id]]
46+
== @ClientRegistrationId
47+
48+
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.
49+
50+
include-code::./UserService[tag=getAuthenticatedUser]
51+
52+
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`]
53+
54+
[[client-registration-id-processor]]
55+
== `ClientRegistrationIdProcessor`
56+
57+
The xref:features/integrations/rest/http-interface.adoc#configuration[configured] javadoc:org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor[] will:
58+
59+
- 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`].
60+
- This adds the javadoc:org.springframework.security.oauth2.client.registration.ClientRegistration#getId()[] to the attributes
61+
62+
The `id` is then processed by:
63+
64+
- `OAuth2ClientHttpRequestInterceptor` for xref:servlet/oauth2/client/authorized-clients.adoc#oauth2-client-rest-client[RestClient Integration]
65+
- 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`.
66+

docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,11 @@ class RestClientConfig {
495495
----
496496
=====
497497

498+
[[oauth2-client-rest-client-interface]]
499+
=== HTTP Interface Integration
500+
501+
Spring Security's OAuth support integrates with xref:features/integrations/rest/http-interface.adoc[].
502+
498503
[[oauth2-client-web-client]]
499504
== [[oauth2Client-webclient-servlet]]WebClient Integration for Servlet Environments
500505

docs/modules/ROOT/pages/whats-new.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Below are the highlights of the release, or you can view https://github.com/spri
77
== Web
88

99
* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
10+
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]

docs/spring-security-docs.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ dependencies {
3939
testImplementation project(':spring-security-config')
4040
testImplementation project(path : ':spring-security-config', configuration : 'tests')
4141
testImplementation project(':spring-security-test')
42+
testImplementation project(':spring-security-oauth2-client')
43+
testImplementation 'com.squareup.okhttp3:mockwebserver'
4244
testImplementation 'com.unboundid:unboundid-ldapsdk'
4345
testImplementation libs.webauthn4j.core
4446
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
@@ -49,6 +51,7 @@ dependencies {
4951

5052
testImplementation 'org.springframework:spring-webmvc'
5153
testImplementation 'jakarta.servlet:jakarta.servlet-api'
54+
testImplementation 'io.mockk:mockk'
5255
testImplementation "org.junit.jupiter:junit-jupiter-api"
5356
testImplementation "org.junit.jupiter:junit-jupiter-params"
5457
testImplementation "org.junit.jupiter:junit-jupiter-engine"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain clients copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.docs.features.integrations.rest.clientregistrationid;
18+
19+
/**
20+
* A user.
21+
* @param login
22+
* @param id
23+
* @param name
24+
* @author Rob Winch
25+
* @see UserService
26+
*/
27+
public record User(String login, int id, String name) {
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain clients copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.docs.features.integrations.rest.clientregistrationid;
18+
19+
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
20+
import org.springframework.web.service.annotation.GetExchange;
21+
import org.springframework.web.service.annotation.HttpExchange;
22+
23+
/**
24+
* Demonstrates a service for {@link ClientRegistrationId} and HTTP Interface clients.
25+
* @author Rob Winch
26+
*/
27+
@HttpExchange
28+
public interface UserService {
29+
30+
// tag::getAuthenticatedUser[]
31+
@GetExchange("/user")
32+
@ClientRegistrationId("github")
33+
User getAuthenticatedUser();
34+
// end::getAuthenticatedUser[]
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain clients copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.docs.features.integrations.rest.configurationrestclient;
18+
19+
import okhttp3.mockwebserver.MockWebServer;
20+
21+
import org.springframework.context.annotation.Bean;
22+
import org.springframework.context.annotation.Configuration;
23+
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
24+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
25+
import org.springframework.security.oauth2.client.web.client.support.OAuth2RestClientHttpServiceGroupConfigurer;
26+
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
27+
import org.springframework.web.service.registry.ImportHttpServices;
28+
29+
import static org.mockito.Mockito.mock;
30+
31+
/**
32+
* Documentation for {@link OAuth2RestClientHttpServiceGroupConfigurer}.
33+
* @author Rob Winch
34+
*/
35+
@Configuration(proxyBeanMethods = false)
36+
@ImportHttpServices(types = UserService.class)
37+
public class RestClientHttpInterfaceIntegrationConfiguration {
38+
39+
// tag::config[]
40+
@Bean
41+
OAuth2RestClientHttpServiceGroupConfigurer securityConfigurer(
42+
OAuth2AuthorizedClientManager manager) {
43+
return OAuth2RestClientHttpServiceGroupConfigurer.from(manager);
44+
}
45+
// end::config[]
46+
47+
@Bean
48+
OAuth2AuthorizedClientManager authorizedClientManager() {
49+
return mock(OAuth2AuthorizedClientManager.class);
50+
}
51+
52+
@Bean
53+
RestClientHttpServiceGroupConfigurer groupConfigurer(MockWebServer server) {
54+
return groups -> {
55+
56+
groups
57+
.forEachClient((group, builder) -> builder
58+
.baseUrl(server.url("").toString())
59+
.defaultHeader("Accept", "application/vnd.github.v3+json"));
60+
};
61+
}
62+
63+
@Bean
64+
MockWebServer mockServer() {
65+
return new MockWebServer();
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain clients copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.docs.features.integrations.rest.configurationrestclient;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
22+
import okhttp3.mockwebserver.MockResponse;
23+
import okhttp3.mockwebserver.MockWebServer;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.MediaType;
30+
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
31+
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
32+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
33+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
34+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
35+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
36+
import org.springframework.test.context.ContextConfiguration;
37+
import org.springframework.test.context.junit.jupiter.SpringExtension;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.mockito.ArgumentMatchers.any;
41+
import static org.mockito.BDDMockito.given;
42+
43+
/**
44+
* Tests RestClient configuration for HTTP Interface clients.
45+
* @author Rob Winch
46+
*/
47+
@ExtendWith(SpringExtension.class)
48+
@ContextConfiguration(classes = RestClientHttpInterfaceIntegrationConfiguration.class)
49+
class RestClientHttpInterfaceIntegrationConfigurationTests {
50+
51+
@Test
52+
void getAuthenticatedUser(@Autowired MockWebServer webServer, @Autowired OAuth2AuthorizedClientManager authorizedClients, @Autowired UserService users)
53+
throws InterruptedException {
54+
ClientRegistration registration = CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("github").build();
55+
56+
Instant issuedAt = Instant.now();
57+
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(5));
58+
OAuth2AccessToken token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "1234",
59+
issuedAt, expiresAt);
60+
OAuth2AuthorizedClient result = new OAuth2AuthorizedClient(registration, "rob", token);
61+
given(authorizedClients.authorize(any())).willReturn(result);
62+
63+
webServer.enqueue(new MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(
64+
"""
65+
{"login": "rob_winch", "id": 1234, "name": "Rob Winch" }
66+
"""));
67+
68+
users.getAuthenticatedUser();
69+
70+
assertThat(webServer.takeRequest().getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + token.getTokenValue());
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain clients copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.docs.features.integrations.rest.configurationwebclient;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
22+
import okhttp3.mockwebserver.MockResponse;
23+
import okhttp3.mockwebserver.MockWebServer;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import reactor.core.publisher.Mono;
27+
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.http.HttpHeaders;
30+
import org.springframework.http.MediaType;
31+
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
32+
import org.springframework.security.docs.features.integrations.rest.clientregistrationid.UserService;
33+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
34+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
35+
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
36+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
37+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
38+
import org.springframework.test.context.ContextConfiguration;
39+
import org.springframework.test.context.junit.jupiter.SpringExtension;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
import static org.mockito.ArgumentMatchers.any;
43+
import static org.mockito.BDDMockito.given;
44+
45+
/**
46+
* Demonstrates configuring RestClient with interface based proxy clients.
47+
* @author Rob Winch
48+
*/
49+
@ExtendWith(SpringExtension.class)
50+
@ContextConfiguration(classes = ServerWebClientHttpInterfaceIntegrationConfiguration.class)
51+
class ServerRestClientHttpInterfaceIntegrationConfigurationTests {
52+
53+
@Test
54+
void getAuthenticatedUser(@Autowired MockWebServer webServer, @Autowired ReactiveOAuth2AuthorizedClientManager authorizedClients, @Autowired UserService users)
55+
throws InterruptedException {
56+
ClientRegistration registration = CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("github").build();
57+
58+
Instant issuedAt = Instant.now();
59+
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(5));
60+
OAuth2AccessToken token = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "1234",
61+
issuedAt, expiresAt);
62+
OAuth2AuthorizedClient result = new OAuth2AuthorizedClient(registration, "rob", token);
63+
given(authorizedClients.authorize(any())).willReturn(Mono.just(result));
64+
65+
webServer.enqueue(new MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(
66+
"""
67+
{"login": "rob_winch", "id": 1234, "name": "Rob Winch" }
68+
"""));
69+
70+
users.getAuthenticatedUser();
71+
72+
assertThat(webServer.takeRequest().getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + token.getTokenValue());
73+
}
74+
75+
}

0 commit comments

Comments
 (0)