Skip to content

Commit 28537fa

Browse files
committed
WebClientReactiveClientCredentialsTokenResponseClient
Fixes: gh-5607
1 parent 89f2874 commit 28537fa

File tree

2 files changed

+233
-0
lines changed

2 files changed

+233
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2002-2018 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 a copy of the License at
7+
*
8+
* http://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+
package org.springframework.security.oauth2.client.endpoint;
17+
18+
import org.springframework.http.HttpHeaders;
19+
import org.springframework.http.MediaType;
20+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
21+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
22+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
23+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
25+
import org.springframework.util.CollectionUtils;
26+
import org.springframework.util.StringUtils;
27+
import org.springframework.web.reactive.function.BodyInserters;
28+
import org.springframework.web.reactive.function.client.WebClient;
29+
import reactor.core.publisher.Mono;
30+
31+
import java.util.Set;
32+
import java.util.function.Consumer;
33+
34+
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
35+
36+
/**
37+
* An implementation of an {@link ReactiveOAuth2AccessTokenResponseClient} that "exchanges"
38+
* an authorization code credential for an access token credential
39+
* at the Authorization Server's Token Endpoint.
40+
*
41+
* @author Rob Winch
42+
* @since 5.1
43+
* @see OAuth2AccessTokenResponseClient
44+
* @see OAuth2AuthorizationCodeGrantRequest
45+
* @see OAuth2AccessTokenResponse
46+
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
47+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
48+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
49+
*/
50+
public class WebClientReactiveClientCredentialsTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
51+
private WebClient webClient = WebClient.builder()
52+
.build();
53+
54+
@Override
55+
public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2ClientCredentialsGrantRequest authorizationGrantRequest)
56+
throws OAuth2AuthenticationException {
57+
58+
return Mono.defer(() -> {
59+
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
60+
61+
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
62+
BodyInserters.FormInserter<String> body = body(authorizationGrantRequest);
63+
64+
return this.webClient.post()
65+
.uri(tokenUri)
66+
.accept(MediaType.APPLICATION_JSON)
67+
.headers(headers(clientRegistration))
68+
.body(body)
69+
.exchange()
70+
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
71+
.map(response -> {
72+
if (response.getAccessToken().getScopes().isEmpty()) {
73+
response = OAuth2AccessTokenResponse.withResponse(response)
74+
.scopes(authorizationGrantRequest.getClientRegistration().getScopes())
75+
.build();
76+
}
77+
return response;
78+
});
79+
});
80+
}
81+
82+
private Consumer<HttpHeaders> headers(ClientRegistration clientRegistration) {
83+
return headers -> {
84+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
85+
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
86+
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
87+
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
88+
}
89+
};
90+
}
91+
92+
private static BodyInserters.FormInserter<String> body(OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
93+
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
94+
BodyInserters.FormInserter<String> body = BodyInserters
95+
.fromFormData(OAuth2ParameterNames.GRANT_TYPE, authorizationGrantRequest.getGrantType().getValue());
96+
Set<String> scopes = clientRegistration.getScopes();
97+
if (!CollectionUtils.isEmpty(scopes)) {
98+
String scope = StringUtils.collectionToDelimitedString(scopes, " ");
99+
body.with(OAuth2ParameterNames.SCOPE, scope);
100+
}
101+
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
102+
body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
103+
body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
104+
}
105+
return body;
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2002-2018 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 a copy of the License at
7+
*
8+
* http://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.oauth2.client.endpoint;
18+
19+
import okhttp3.mockwebserver.MockResponse;
20+
import okhttp3.mockwebserver.MockWebServer;
21+
import okhttp3.mockwebserver.RecordedRequest;
22+
import org.junit.After;
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
28+
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
29+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
30+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
31+
32+
import static org.assertj.core.api.Assertions.*;
33+
34+
/**
35+
* @author Rob Winch
36+
*/
37+
public class WebClientReactiveClientCredentialsTokenResponseClientTests {
38+
39+
private MockWebServer server;
40+
41+
private WebClientReactiveClientCredentialsTokenResponseClient client = new WebClientReactiveClientCredentialsTokenResponseClient();
42+
43+
private ClientRegistration.Builder clientRegistration;
44+
45+
@Before
46+
public void setup() throws Exception {
47+
this.server = new MockWebServer();
48+
this.server.start();
49+
50+
this.clientRegistration = TestClientRegistrations
51+
.clientCredentials()
52+
.tokenUri(this.server.url("/oauth2/token").uri().toASCIIString());
53+
}
54+
55+
@After
56+
public void cleanup() throws Exception {
57+
this.server.shutdown();
58+
}
59+
60+
@Test
61+
public void getTokenResponseWhenHeaderThenSuccess() throws Exception {
62+
enqueueJson("{\n"
63+
+ " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
64+
+ " \"token_type\":\"bearer\",\n"
65+
+ " \"expires_in\":3600,\n"
66+
+ " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n"
67+
+ " \"scope\":\"create\"\n"
68+
+ "}");
69+
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(this.clientRegistration
70+
.build());
71+
72+
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
73+
RecordedRequest actualRequest = this.server.takeRequest();
74+
String body = actualRequest.getUtf8Body();
75+
76+
assertThat(response.getAccessToken()).isNotNull();
77+
assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=");
78+
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
79+
}
80+
81+
@Test
82+
public void getTokenResponseWhenPostThenSuccess() throws Exception {
83+
ClientRegistration registration = this.clientRegistration
84+
.clientAuthenticationMethod(ClientAuthenticationMethod.POST)
85+
.build();
86+
enqueueJson("{\n"
87+
+ " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
88+
+ " \"token_type\":\"bearer\",\n"
89+
+ " \"expires_in\":3600,\n"
90+
+ " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n"
91+
+ " \"scope\":\"create\"\n"
92+
+ "}");
93+
94+
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration);
95+
96+
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
97+
String body = this.server.takeRequest().getUtf8Body();
98+
99+
assertThat(response.getAccessToken()).isNotNull();
100+
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser&client_id=client-id&client_secret=client-secret");
101+
}
102+
103+
@Test
104+
public void getTokenResponseWhenNoScopeThenClientRegistrationScopesDefaulted() {
105+
ClientRegistration registration = this.clientRegistration.build();
106+
enqueueJson("{\n"
107+
+ " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
108+
+ " \"token_type\":\"bearer\",\n"
109+
+ " \"expires_in\":3600,\n"
110+
+ " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\"\n"
111+
+ "}");
112+
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration);
113+
114+
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
115+
116+
assertThat(response.getAccessToken().getScopes()).isEqualTo(registration.getScopes());
117+
}
118+
119+
120+
private void enqueueJson(String body) {
121+
MockResponse response = new MockResponse()
122+
.setBody(body)
123+
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
124+
this.server.enqueue(response);
125+
}
126+
}

0 commit comments

Comments
 (0)