Skip to content

Commit 840d3aa

Browse files
philsttrjgrandja
authored andcommitted
Polish #7589
Rename OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager to AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager. Handle empty mono returned from contextAttributesMapper. Handle empty map returned from contextAttributesMapper. Fix DefaultContextAttributesMapper so that it doesn't access ServerWebExchange. Fix unit tests so that they pass. Use StepVerifier in unit tests, rather than .subscribe(). Fixes gh-7569
1 parent 4c5c4f6 commit 840d3aa

File tree

2 files changed

+87
-80
lines changed

2 files changed

+87
-80
lines changed
Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,82 +15,88 @@
1515
*/
1616
package org.springframework.security.oauth2.client;
1717

18-
import org.springframework.lang.Nullable;
1918
import org.springframework.security.core.Authentication;
2019
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
21-
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
2220
import org.springframework.util.Assert;
23-
import org.springframework.util.CollectionUtils;
24-
import org.springframework.util.StringUtils;
25-
import org.springframework.web.server.ServerWebExchange;
2621
import reactor.core.publisher.Mono;
2722

2823
import java.util.Collections;
29-
import java.util.HashMap;
3024
import java.util.Map;
31-
import java.util.Optional;
3225
import java.util.function.Function;
3326

3427
/**
3528
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
3629
* that is capable of operating outside of a {@code ServerHttpRequest} context,
3730
* e.g. in a scheduled/background thread and/or in the service-tier.
3831
*
32+
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
33+
*
3934
* @author Ankur Pathak
35+
* @author Phil Clay
4036
* @see ReactiveOAuth2AuthorizedClientManager
4137
* @see ReactiveOAuth2AuthorizedClientProvider
4238
* @see ReactiveOAuth2AuthorizedClientService
43-
* @since 5.3
39+
* @since 5.2.2
4440
*/
45-
public final class OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
41+
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
42+
implements ReactiveOAuth2AuthorizedClientManager {
43+
4644
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
4745
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
4846
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
4947
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
5048

5149
/**
52-
* Constructs an {@code OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
50+
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
5351
*
5452
* @param clientRegistrationRepository the repository of client registrations
5553
* @param authorizedClientService the authorized client service
5654
*/
57-
public OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository,
55+
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
56+
ReactiveClientRegistrationRepository clientRegistrationRepository,
5857
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
5958
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
6059
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
6160
this.clientRegistrationRepository = clientRegistrationRepository;
6261
this.authorizedClientService = authorizedClientService;
6362
}
6463

65-
@Nullable
6664
@Override
6765
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
6866
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
67+
68+
return createAuthorizationContext(authorizeRequest)
69+
.flatMap(this::authorizeAndSave);
70+
}
71+
72+
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
6973
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
70-
OAuth2AuthorizedClient authorizedClient = authorizeRequest.getAuthorizedClient();
7174
Authentication principal = authorizeRequest.getPrincipal();
72-
// @formatter:off
73-
return Mono.justOrEmpty(authorizedClient)
75+
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
7476
.map(OAuth2AuthorizationContext::withAuthorizedClient)
7577
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
76-
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
77-
.map(OAuth2AuthorizationContext::withAuthorizedClient)
78-
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration)))
79-
)
80-
.switchIfEmpty(Mono.error(new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
81-
)
82-
)
78+
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
79+
.map(OAuth2AuthorizationContext::withAuthorizedClient)
80+
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
81+
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
8382
.flatMap(contextBuilder -> this.contextAttributesMapper.apply(authorizeRequest)
84-
.filter(contextAttributes-> !CollectionUtils.isEmpty(contextAttributes))
85-
.map(contextAttributes -> contextBuilder.principal(principal)
86-
.attributes(attributes -> {
87-
attributes.putAll(contextAttributes);
88-
}).build())
89-
).flatMap(authorizationContext -> this.authorizedClientProvider.authorize(authorizationContext)
90-
.doOnNext(_authorizedClient -> authorizedClientService.saveAuthorizedClient(_authorizedClient, principal))
91-
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(Optional.ofNullable(authorizationContext.getAuthorizedClient()))))
92-
);
93-
// @formatter:on
83+
.defaultIfEmpty(Collections.emptyMap())
84+
.map(contextAttributes -> {
85+
OAuth2AuthorizationContext.Builder builder = contextBuilder.principal(principal);
86+
if (!contextAttributes.isEmpty()) {
87+
builder = builder.attributes(attributes -> attributes.putAll(contextAttributes));
88+
}
89+
return builder.build();
90+
}));
91+
}
92+
93+
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
94+
return this.authorizedClientProvider.authorize(authorizationContext)
95+
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
96+
authorizedClient,
97+
authorizationContext.getPrincipal())
98+
.thenReturn(authorizedClient))
99+
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
94100
}
95101

96102
/**
@@ -115,33 +121,17 @@ public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map
115121
this.contextAttributesMapper = contextAttributesMapper;
116122
}
117123

118-
private static Mono<ServerWebExchange> currentServerWebExchange() {
119-
return Mono.subscriberContext()
120-
.filter(c -> c.hasKey(ServerWebExchange.class))
121-
.map(c -> c.get(ServerWebExchange.class));
122-
}
123-
124124
/**
125125
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
126126
*/
127127
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
128128

129+
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper =
130+
new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
131+
129132
@Override
130133
public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest) {
131-
ServerWebExchange serverWebExchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName());
132-
return Mono.justOrEmpty(serverWebExchange)
133-
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
134-
.flatMap(exchange -> {
135-
Map<String, Object> contextAttributes = Collections.emptyMap();
136-
String scope = exchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.SCOPE);
137-
if (StringUtils.hasText(scope)) {
138-
contextAttributes = new HashMap<>();
139-
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
140-
StringUtils.delimitedListToStringArray(scope, " "));
141-
}
142-
return Mono.just(contextAttributes);
143-
})
144-
.defaultIfEmpty(Collections.emptyMap());
134+
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
145135
}
146136
}
147137
}
Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,39 +28,49 @@
2828
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
2929
import reactor.core.publisher.Mono;
3030
import reactor.test.StepVerifier;
31+
import reactor.test.publisher.PublisherProbe;
3132

33+
import java.util.Map;
3234
import java.util.function.Function;
3335

3436
import static org.assertj.core.api.Assertions.assertThat;
3537
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3638
import static org.mockito.ArgumentMatchers.any;
3739
import static org.mockito.ArgumentMatchers.eq;
38-
import static org.mockito.Mockito.*;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.never;
42+
import static org.mockito.Mockito.verify;
43+
import static org.mockito.Mockito.when;
3944

4045
/**
41-
* Tests for {@link OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}.
46+
* Tests for {@link AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}.
4247
*
4348
* @author Ankur Pathak
49+
* @author Phil Clay
4450
*/
45-
public class OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManagerTests {
51+
public class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManagerTests {
4652
private ReactiveClientRegistrationRepository clientRegistrationRepository;
4753
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
4854
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
49-
private Function contextAttributesMapper;
50-
private OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
55+
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper;
56+
private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
5157
private ClientRegistration clientRegistration;
5258
private Authentication principal;
5359
private OAuth2AuthorizedClient authorizedClient;
5460
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
61+
private PublisherProbe<Void> saveAuthorizedClientProbe;
5562

5663
@SuppressWarnings("unchecked")
5764
@Before
5865
public void setup() {
5966
this.clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
6067
this.authorizedClientService = mock(ReactiveOAuth2AuthorizedClientService.class);
68+
this.saveAuthorizedClientProbe = PublisherProbe.empty();
69+
when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(this.saveAuthorizedClientProbe.mono());
6170
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
6271
this.contextAttributesMapper = mock(Function.class);
63-
this.authorizedClientManager = new OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
72+
when(this.contextAttributesMapper.apply(any())).thenReturn(Mono.empty());
73+
this.authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
6474
this.clientRegistrationRepository, this.authorizedClientService);
6575
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
6676
this.authorizedClientManager.setContextAttributesMapper(this.contextAttributesMapper);
@@ -73,23 +83,23 @@ public void setup() {
7383

7484
@Test
7585
public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
76-
assertThatThrownBy(() -> new OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(null, this.authorizedClientService))
86+
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(null, this.authorizedClientService))
7787
.isInstanceOf(IllegalArgumentException.class)
78-
.hasMessage("reactiveClientRegistrationRepository cannot be null");
88+
.hasMessage("clientRegistrationRepository cannot be null");
7989
}
8090

8191
@Test
8292
public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
83-
assertThatThrownBy(() -> new OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(this.clientRegistrationRepository, null))
93+
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(this.clientRegistrationRepository, null))
8494
.isInstanceOf(IllegalArgumentException.class)
85-
.hasMessage("reactiveAuthorizedClientService cannot be null");
95+
.hasMessage("authorizedClientService cannot be null");
8696
}
8797

8898
@Test
8999
public void setAuthorizedClientProviderWhenNullThenThrowIllegalArgumentException() {
90100
assertThatThrownBy(() -> this.authorizedClientManager.setAuthorizedClientProvider(null))
91101
.isInstanceOf(IllegalArgumentException.class)
92-
.hasMessage("reactiveAuthorizedClientProvider cannot be null");
102+
.hasMessage("authorizedClientProvider cannot be null");
93103
}
94104

95105
@Test
@@ -132,7 +142,7 @@ public void authorizeWhenNotAuthorizedAndUnsupportedProviderThenNotAuthorized()
132142
.build();
133143
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
134144

135-
authorizedClient.subscribe();
145+
StepVerifier.create(authorizedClient).verifyComplete();
136146

137147
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
138148
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
@@ -142,7 +152,6 @@ public void authorizeWhenNotAuthorizedAndUnsupportedProviderThenNotAuthorized()
142152
assertThat(authorizationContext.getAuthorizedClient()).isNull();
143153
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
144154

145-
StepVerifier.create(authorizedClient).expectComplete();
146155
verify(this.authorizedClientService, never()).saveAuthorizedClient(
147156
any(OAuth2AuthorizedClient.class), eq(this.principal));
148157
}
@@ -163,7 +172,9 @@ public void authorizeWhenNotAuthorizedAndSupportedProviderThenAuthorized() {
163172
.build();
164173
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
165174

166-
authorizedClient.subscribe();
175+
StepVerifier.create(authorizedClient)
176+
.expectNext(this.authorizedClient)
177+
.verifyComplete();
167178

168179
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
169180
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
@@ -173,9 +184,9 @@ public void authorizeWhenNotAuthorizedAndSupportedProviderThenAuthorized() {
173184
assertThat(authorizationContext.getAuthorizedClient()).isNull();
174185
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
175186

176-
StepVerifier.create(authorizedClient).expectNextCount(1).assertNext(x -> assertThat(x).isSameAs(this.authorizedClient));
177187
verify(this.authorizedClientService).saveAuthorizedClient(
178188
eq(this.authorizedClient), eq(this.principal));
189+
this.saveAuthorizedClientProbe.assertWasSubscribed();
179190
}
180191

181192
@SuppressWarnings("unchecked")
@@ -197,8 +208,9 @@ public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
197208
.build();
198209
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
199210

200-
authorizedClient.subscribe();
201-
211+
StepVerifier.create(authorizedClient)
212+
.expectNext(reauthorizedClient)
213+
.verifyComplete();
202214
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
203215
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
204216

@@ -207,9 +219,9 @@ public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
207219
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
208220
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
209221

210-
StepVerifier.create(authorizedClient).expectNextCount(1).assertNext(x -> assertThat(x).isSameAs(this.authorizedClient));
211222
verify(this.authorizedClientService).saveAuthorizedClient(
212223
eq(reauthorizedClient), eq(this.principal));
224+
this.saveAuthorizedClientProbe.assertWasSubscribed();
213225
}
214226

215227
@SuppressWarnings("unchecked")
@@ -221,8 +233,9 @@ public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
221233
.build();
222234
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
223235

224-
authorizedClient.subscribe();
225-
236+
StepVerifier.create(authorizedClient)
237+
.expectNext(this.authorizedClient)
238+
.verifyComplete();
226239
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
227240
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
228241

@@ -231,7 +244,6 @@ public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
231244
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
232245
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
233246

234-
StepVerifier.create(authorizedClient).expectNextCount(1).assertNext(x -> assertThat(x).isSameAs(this.authorizedClient));
235247
verify(this.authorizedClientService, never()).saveAuthorizedClient(
236248
any(OAuth2AuthorizedClient.class), eq(this.principal));
237249
}
@@ -250,7 +262,9 @@ public void reauthorizeWhenSupportedProviderThenReauthorized() {
250262
.build();
251263
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
252264

253-
authorizedClient.subscribe();
265+
StepVerifier.create(authorizedClient)
266+
.expectNext(reauthorizedClient)
267+
.verifyComplete();
254268

255269
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
256270
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
@@ -260,9 +274,9 @@ public void reauthorizeWhenSupportedProviderThenReauthorized() {
260274
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
261275
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
262276

263-
StepVerifier.create(authorizedClient).expectNextCount(1).assertNext(x -> assertThat(x).isSameAs(this.authorizedClient));
264277
verify(this.authorizedClientService).saveAuthorizedClient(
265278
eq(reauthorizedClient), eq(this.principal));
279+
this.saveAuthorizedClientProbe.assertWasSubscribed();
266280
}
267281

268282
@SuppressWarnings("unchecked")
@@ -274,14 +288,20 @@ public void reauthorizeWhenRequestAttributeScopeThenMappedToContext() {
274288

275289
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
276290

277-
278291
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
279292
.principal(this.principal)
280293
.attribute(OAuth2ParameterNames.SCOPE, "read write")
281294
.build();
295+
296+
this.authorizedClientManager.setContextAttributesMapper(new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
282297
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
283298

284-
authorizedClient.subscribe();
299+
StepVerifier.create(authorizedClient)
300+
.expectNext(reauthorizedClient)
301+
.verifyComplete();
302+
verify(this.authorizedClientService).saveAuthorizedClient(
303+
eq(reauthorizedClient), eq(this.principal));
304+
this.saveAuthorizedClientProbe.assertWasSubscribed();
285305

286306
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
287307

@@ -293,8 +313,5 @@ public void reauthorizeWhenRequestAttributeScopeThenMappedToContext() {
293313
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
294314
assertThat(requestScopeAttribute).contains("read", "write");
295315

296-
StepVerifier.create(authorizedClient).expectNextCount(1).assertNext(x -> assertThat(x).isSameAs(this.authorizedClient));
297-
verify(this.authorizedClientService).saveAuthorizedClient(
298-
eq(reauthorizedClient), eq(this.principal));
299316
}
300317
}

0 commit comments

Comments
 (0)