diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/webclient.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/webclient.adoc index e55fa91ded..d33e7b6c01 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/webclient.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/webclient.adoc @@ -38,7 +38,9 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients); // (optional) explicitly opt into using the oauth2Login to provide an access token implicitly - oauth.setDefaultOAuth2AuthorizedClient(true); + // oauth.setDefaultOAuth2AuthorizedClient(true); + // (optional) set a default ClientRegistration.registrationId + // oauth.setDefaultClientRegistrationId("client-registration-id"); return WebClient.builder() .filter(oauth) .build(); @@ -48,7 +50,8 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, [[webclient-implicit]] == Implicit OAuth2AuthorizedClient -If we set `defaultOAuth2AuthorizedClient` to `true` in our setup and the user authenticated with oauth2Login (i.e. OIDC), then the current authentication is used to automatically provide the access token. +If we set `defaultOAuth2AuthorizedClient` to `true`in our setup and the user authenticated with oauth2Login (i.e. OIDC), then the current authentication is used to automatically provide the access token. +Alternatively, if we set `defaultClientRegistrationId` to a valid `ClientRegistration` id, that registration is used to provide the access token. This is convenient, but in environments where not all endpoints should get the access token, it is dangerous (you might provide the wrong access token to an endpoint). [source,java] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/additional-topics/webclient.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/additional-topics/webclient.adoc index fceb8f1b66..f9b91d0a38 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/additional-topics/webclient.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/additional-topics/webclient.adoc @@ -39,9 +39,11 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients); // (optional) explicitly opt into using the oauth2Login to provide an access token implicitly - oauth.setDefaultOAuth2AuthorizedClient(true); + // oauth.setDefaultOAuth2AuthorizedClient(true); + // (optional) set a default ClientRegistration.registrationId + // oauth.setDefaultClientRegistrationId("client-registration-id"); return WebClient.builder() - .filter(oauth) + .apply(oauth2.oauth2Configuration()) .build(); } ---- @@ -50,6 +52,7 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, == Implicit OAuth2AuthorizedClient If we set `defaultOAuth2AuthorizedClient` to `true` in our setup and the user authenticated with oauth2Login (i.e. OIDC), then the current authentication is used to automatically provide the access token. +Alternatively, if we set `defaultClientRegistrationId` to a valid `ClientRegistration` id, that registration is used to provide the access token. This is convenient, but in environments where not all endpoints should get the access token, it is dangerous (you might provide the wrong access token to an endpoint). [source,java] diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientResolver.java index 6b381c6d2b..df1a365566 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientResolver.java @@ -55,6 +55,8 @@ class OAuth2AuthorizedClientResolver { private boolean defaultOAuth2AuthorizedClient; + private String defaultClientRegistrationId; + public OAuth2AuthorizedClientResolver( ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { @@ -75,6 +77,15 @@ class OAuth2AuthorizedClientResolver { this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; } + /** + * If set, will be used as the default {@link ClientRegistration#getRegistrationId()}. It is + * recommended to be cautious with this feature since all HTTP requests will receive the access token. + * @param clientRegistrationId the id to use + */ + public void setDefaultClientRegistrationId(String clientRegistrationId) { + this.defaultClientRegistrationId = clientRegistrationId; + } + /** * Sets the {@link ReactiveOAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for * client_credentials grant. @@ -92,6 +103,7 @@ class OAuth2AuthorizedClientResolver { .switchIfEmpty(currentAuthentication()); Mono defaultedRegistrationId = Mono.justOrEmpty(clientRegistrationId) + .switchIfEmpty(Mono.justOrEmpty(this.defaultClientRegistrationId)) .switchIfEmpty(clientRegistrationId(defaultedAuthentication)); Mono> defaultedExchange = Mono.justOrEmpty(exchange) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index 3d66f295a8..ef92036690 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -188,6 +188,15 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements this.authorizedClientResolver.setDefaultOAuth2AuthorizedClient(defaultOAuth2AuthorizedClient); } + /** + * If set, will be used as the default {@link ClientRegistration#getRegistrationId()}. It is + * recommended to be cautious with this feature since all HTTP requests will receive the access token. + * @param clientRegistrationId the id to use + */ + public void setDefaultClientRegistrationId(String clientRegistrationId) { + this.authorizedClientResolver.setDefaultClientRegistrationId(clientRegistrationId); + } + /** * Sets the {@link ReactiveOAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for * client_credentials grant. diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index 9c8e088458..6914363aa5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -121,6 +121,8 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement private boolean defaultOAuth2AuthorizedClient; + private String defaultClientRegistrationId; + public ServletOAuth2AuthorizedClientExchangeFilterFunction() {} public ServletOAuth2AuthorizedClientExchangeFilterFunction( @@ -152,6 +154,16 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; } + + /** + * If set, will be used as the default {@link ClientRegistration#getRegistrationId()}. It is + * recommended to be cautious with this feature since all HTTP requests will receive the access token. + * @param clientRegistrationId the id to use + */ + public void setDefaultClientRegistrationId(String clientRegistrationId) { + this.defaultClientRegistrationId = clientRegistrationId; + } + /** * Configures the builder with {@link #defaultRequest()} and adds this as a {@link ExchangeFilterFunction} * @return the {@link Consumer} to configure the builder @@ -295,6 +307,9 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement Authentication authentication = getAuthentication(attrs); String clientRegistrationId = getClientRegistrationId(attrs); + if (clientRegistrationId == null) { + clientRegistrationId = this.defaultClientRegistrationId; + } if (clientRegistrationId == null && this.defaultOAuth2AuthorizedClient && authentication instanceof OAuth2AuthenticationToken) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientResolver.java index a90f65e9c5..aafe7ae6dc 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientResolver.java @@ -55,6 +55,8 @@ class OAuth2AuthorizedClientResolver { private boolean defaultOAuth2AuthorizedClient; + private String defaultClientRegistrationId; + public OAuth2AuthorizedClientResolver( ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { @@ -75,6 +77,15 @@ class OAuth2AuthorizedClientResolver { this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; } + /** + * If set, will be used as the default {@link ClientRegistration#getRegistrationId()}. It is + * recommended to be cautious with this feature since all HTTP requests will receive the access token. + * @param clientRegistrationId the id to use + */ + public void setDefaultClientRegistrationId(String clientRegistrationId) { + this.defaultClientRegistrationId = clientRegistrationId; + } + /** * Sets the {@link ReactiveOAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for * client_credentials grant. @@ -92,6 +103,7 @@ class OAuth2AuthorizedClientResolver { .switchIfEmpty(currentAuthentication()); Mono defaultedRegistrationId = Mono.justOrEmpty(clientRegistrationId) + .switchIfEmpty(Mono.justOrEmpty(this.defaultClientRegistrationId)) .switchIfEmpty(clientRegistrationId(defaultedAuthentication)) .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("The clientRegistrationId could not be resolved. Please provide one"))); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java index 5a88f3a14b..f6eefec2c6 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -300,6 +300,29 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { assertThat(getBody(request0)).isEmpty(); } + @Test + public void filterWhenDefaultClientRegistrationIdThenAuthorizedClientResolved() { + this.function.setDefaultClientRegistrationId(this.registration.getRegistrationId()); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt(), this.accessToken.getExpiresAt()); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration, + "principalName", this.accessToken, refreshToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.just(authorizedClient)); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(Mono.just(this.registration)); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + @Test public void filterWhenClientRegistrationIdFromAuthenticationThenAuthorizedClientResolved() { this.function.setDefaultOAuth2AuthorizedClient(true); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java index b511e0d74f..1431864f85 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -296,6 +296,28 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); } + @Test + public void defaultRequestWhenDefaultClientRegistrationIdThenAuthorizedClient() { + this.registration = TestClientRegistrations.clientCredentials().build(); + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository, + this.authorizedClientRepository); + this.function.setDefaultClientRegistrationId(this.registration.getRegistrationId()); + this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(this.registration); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses + .accessTokenResponse().build(); + when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn( + accessTokenResponse); + + Map attrs = getDefaultRequestAttributes(); + OAuth2AuthorizedClient authorizedClient = getOAuth2AuthorizedClient(attrs); + + assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration); + assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser"); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); + } + @Test public void defaultRequestWhenClientIdNotFoundThenIllegalArgumentException() { this.registration = TestClientRegistrations.clientCredentials().build();