WebClient OAuth2 Support for defaultClientRegistrationId

Fixes: gh-5872
This commit is contained in:
Rob Winch 2018-09-19 11:27:56 -05:00
parent f48055ce47
commit dcbf762a0b
8 changed files with 103 additions and 4 deletions

View File

@ -38,7 +38,9 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients); new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
// (optional) explicitly opt into using the oauth2Login to provide an access token implicitly // (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() return WebClient.builder()
.filter(oauth) .filter(oauth)
.build(); .build();
@ -48,7 +50,8 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
[[webclient-implicit]] [[webclient-implicit]]
== Implicit OAuth2AuthorizedClient == 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). 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] [source,java]

View File

@ -39,9 +39,11 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients); new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
// (optional) explicitly opt into using the oauth2Login to provide an access token implicitly // (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() return WebClient.builder()
.filter(oauth) .apply(oauth2.oauth2Configuration())
.build(); .build();
} }
---- ----
@ -50,6 +52,7 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
== Implicit OAuth2AuthorizedClient == 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). 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] [source,java]

View File

@ -55,6 +55,8 @@ class OAuth2AuthorizedClientResolver {
private boolean defaultOAuth2AuthorizedClient; private boolean defaultOAuth2AuthorizedClient;
private String defaultClientRegistrationId;
public OAuth2AuthorizedClientResolver( public OAuth2AuthorizedClientResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository, ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
@ -75,6 +77,15 @@ class OAuth2AuthorizedClientResolver {
this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; 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 * Sets the {@link ReactiveOAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for
* client_credentials grant. * client_credentials grant.
@ -92,6 +103,7 @@ class OAuth2AuthorizedClientResolver {
.switchIfEmpty(currentAuthentication()); .switchIfEmpty(currentAuthentication());
Mono<String> defaultedRegistrationId = Mono.justOrEmpty(clientRegistrationId) Mono<String> defaultedRegistrationId = Mono.justOrEmpty(clientRegistrationId)
.switchIfEmpty(Mono.justOrEmpty(this.defaultClientRegistrationId))
.switchIfEmpty(clientRegistrationId(defaultedAuthentication)); .switchIfEmpty(clientRegistrationId(defaultedAuthentication));
Mono<Optional<ServerWebExchange>> defaultedExchange = Mono.justOrEmpty(exchange) Mono<Optional<ServerWebExchange>> defaultedExchange = Mono.justOrEmpty(exchange)

View File

@ -188,6 +188,15 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
this.authorizedClientResolver.setDefaultOAuth2AuthorizedClient(defaultOAuth2AuthorizedClient); 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 * Sets the {@link ReactiveOAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for
* client_credentials grant. * client_credentials grant.

View File

@ -121,6 +121,8 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
private boolean defaultOAuth2AuthorizedClient; private boolean defaultOAuth2AuthorizedClient;
private String defaultClientRegistrationId;
public ServletOAuth2AuthorizedClientExchangeFilterFunction() {} public ServletOAuth2AuthorizedClientExchangeFilterFunction() {}
public ServletOAuth2AuthorizedClientExchangeFilterFunction( public ServletOAuth2AuthorizedClientExchangeFilterFunction(
@ -152,6 +154,16 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; 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} * Configures the builder with {@link #defaultRequest()} and adds this as a {@link ExchangeFilterFunction}
* @return the {@link Consumer} to configure the builder * @return the {@link Consumer} to configure the builder
@ -295,6 +307,9 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
Authentication authentication = getAuthentication(attrs); Authentication authentication = getAuthentication(attrs);
String clientRegistrationId = getClientRegistrationId(attrs); String clientRegistrationId = getClientRegistrationId(attrs);
if (clientRegistrationId == null) {
clientRegistrationId = this.defaultClientRegistrationId;
}
if (clientRegistrationId == null if (clientRegistrationId == null
&& this.defaultOAuth2AuthorizedClient && this.defaultOAuth2AuthorizedClient
&& authentication instanceof OAuth2AuthenticationToken) { && authentication instanceof OAuth2AuthenticationToken) {

View File

@ -55,6 +55,8 @@ class OAuth2AuthorizedClientResolver {
private boolean defaultOAuth2AuthorizedClient; private boolean defaultOAuth2AuthorizedClient;
private String defaultClientRegistrationId;
public OAuth2AuthorizedClientResolver( public OAuth2AuthorizedClientResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository, ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
@ -75,6 +77,15 @@ class OAuth2AuthorizedClientResolver {
this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; 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 * Sets the {@link ReactiveOAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for
* client_credentials grant. * client_credentials grant.
@ -92,6 +103,7 @@ class OAuth2AuthorizedClientResolver {
.switchIfEmpty(currentAuthentication()); .switchIfEmpty(currentAuthentication());
Mono<String> defaultedRegistrationId = Mono.justOrEmpty(clientRegistrationId) Mono<String> defaultedRegistrationId = Mono.justOrEmpty(clientRegistrationId)
.switchIfEmpty(Mono.justOrEmpty(this.defaultClientRegistrationId))
.switchIfEmpty(clientRegistrationId(defaultedAuthentication)) .switchIfEmpty(clientRegistrationId(defaultedAuthentication))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("The clientRegistrationId could not be resolved. Please provide one"))); .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("The clientRegistrationId could not be resolved. Please provide one")));

View File

@ -300,6 +300,29 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
assertThat(getBody(request0)).isEmpty(); 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<ClientRequest> 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 @Test
public void filterWhenClientRegistrationIdFromAuthenticationThenAuthorizedClientResolved() { public void filterWhenClientRegistrationIdFromAuthenticationThenAuthorizedClientResolved() {
this.function.setDefaultOAuth2AuthorizedClient(true); this.function.setDefaultOAuth2AuthorizedClient(true);

View File

@ -296,6 +296,28 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); 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<String, Object> 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 @Test
public void defaultRequestWhenClientIdNotFoundThenIllegalArgumentException() { public void defaultRequestWhenClientIdNotFoundThenIllegalArgumentException() {
this.registration = TestClientRegistrations.clientCredentials().build(); this.registration = TestClientRegistrations.clientCredentials().build();