Skip to content

Commit c68cf99

Browse files
committed
Add OAuth2AuthorizedClientExchangeFilterFunction
Fixes: gh-5386
1 parent 2658577 commit c68cf99

File tree

3 files changed

+232
-0
lines changed

3 files changed

+232
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.web.reactive.function.client;
18+
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
21+
import org.springframework.web.reactive.function.client.ClientRequest;
22+
import org.springframework.web.reactive.function.client.ClientResponse;
23+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
24+
import org.springframework.web.reactive.function.client.ExchangeFunction;
25+
import reactor.core.publisher.Mono;
26+
27+
import java.util.Map;
28+
import java.util.Optional;
29+
import java.util.function.Consumer;
30+
31+
/**
32+
* Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the
33+
* token as a Bearer Token.
34+
*
35+
* @author Rob Winch
36+
* @since 5.1
37+
*/
38+
public final class OAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
39+
/**
40+
* The request attribute name used to locate the {@link OAuth2AuthorizedClient}.
41+
*/
42+
private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName();
43+
44+
/**
45+
* Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for
46+
* providing the Bearer Token. Example usage:
47+
*
48+
* <pre>
49+
* Mono<String> response = this.webClient
50+
* .get()
51+
* .uri(uri)
52+
* .attributes(oauth2AuthorizedClient(authorizedClient))
53+
* // ...
54+
* .retrieve()
55+
* .bodyToMono(String.class);
56+
* </pre>
57+
*
58+
* @param authorizedClient the {@link OAuth2AuthorizedClient} to use.
59+
* @return the {@link Consumer} to populate the
60+
*/
61+
public static Consumer<Map<String, Object>> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) {
62+
return attributes -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient);
63+
}
64+
65+
@Override
66+
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
67+
Optional<OAuth2AuthorizedClient> attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME)
68+
.map(OAuth2AuthorizedClient.class::cast);
69+
return attribute
70+
.map(authorizedClient -> bearer(request, authorizedClient))
71+
.map(next::exchange)
72+
.orElseGet(() -> next.exchange(request));
73+
}
74+
75+
private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) {
76+
return ClientRequest.from(request)
77+
.headers(bearerToken(authorizedClient.getAccessToken().getTokenValue()))
78+
.build();
79+
}
80+
81+
private Consumer<HttpHeaders> bearerToken(String token) {
82+
return headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.web.reactive.function.client;
18+
19+
import static org.mockito.Mockito.mock;
20+
21+
import org.springframework.web.reactive.function.client.ClientRequest;
22+
import org.springframework.web.reactive.function.client.ClientResponse;
23+
import org.springframework.web.reactive.function.client.ExchangeFunction;
24+
25+
import reactor.core.publisher.Mono;
26+
27+
/**
28+
* @author Rob Winch
29+
* @since 5.1
30+
*/
31+
public class MockExchangeFunction implements ExchangeFunction {
32+
private ClientRequest request;
33+
34+
private ClientResponse response = mock(ClientResponse.class);
35+
36+
public ClientRequest getRequest() {
37+
return this.request;
38+
}
39+
40+
@Override
41+
public Mono<ClientResponse> exchange(ClientRequest request) {
42+
return Mono.defer(() -> {
43+
this.request = request;
44+
return Mono.just(this.response);
45+
});
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.web.reactive.function.client;
18+
19+
import org.junit.Test;
20+
import org.springframework.http.HttpHeaders;
21+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
22+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
23+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
24+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
25+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
26+
import org.springframework.web.reactive.function.client.ClientRequest;
27+
28+
import java.net.URI;
29+
import java.time.Duration;
30+
import java.time.Instant;
31+
32+
import static org.assertj.core.api.Assertions.*;
33+
import static org.springframework.http.HttpMethod.GET;
34+
import static org.springframework.security.oauth2.client.web.reactive.function.client.OAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
35+
36+
/**
37+
* @author Rob Winch
38+
* @since 5.1
39+
*/
40+
public class OAuth2AuthorizedClientExchangeFilterFunctionTests {
41+
private OAuth2AuthorizedClientExchangeFilterFunction function = new OAuth2AuthorizedClientExchangeFilterFunction();
42+
43+
private MockExchangeFunction exchange = new MockExchangeFunction();
44+
45+
private ClientRegistration github = ClientRegistration.withRegistrationId("github")
46+
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
47+
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
48+
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
49+
.scope("read:user")
50+
.authorizationUri("https://github.com/login/oauth/authorize")
51+
.tokenUri("https://github.com/login/oauth/access_token")
52+
.userInfoUri("https://api.github.com/user")
53+
.userNameAttributeName("id")
54+
.clientName("GitHub")
55+
.clientId("clientId")
56+
.clientSecret("clientSecret")
57+
.build();
58+
59+
private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
60+
"token",
61+
Instant.now(),
62+
Instant.now().plus(Duration.ofDays(1)));
63+
64+
@Test
65+
public void filterWhenAuthorizedClientNullThenAuthorizationHeaderNull() {
66+
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
67+
.build();
68+
69+
this.function.filter(request, this.exchange).block();
70+
71+
assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull();
72+
}
73+
74+
@Test
75+
public void filterWhenAuthorizedClientThenAuthorizationHeader() {
76+
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github,
77+
"principalName", this.accessToken);
78+
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
79+
.attributes(oauth2AuthorizedClient(authorizedClient))
80+
.build();
81+
82+
this.function.filter(request, this.exchange).block();
83+
84+
assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue());
85+
}
86+
87+
@Test
88+
public void filterWhenExistingAuthorizationThenSingleAuthorizationHeader() {
89+
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github,
90+
"principalName", this.accessToken);
91+
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
92+
.header(HttpHeaders.AUTHORIZATION, "Existing")
93+
.attributes(oauth2AuthorizedClient(authorizedClient))
94+
.build();
95+
96+
this.function.filter(request, this.exchange).block();
97+
98+
HttpHeaders headers = this.exchange.getRequest().headers();
99+
assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue());
100+
}
101+
}

0 commit comments

Comments
 (0)