Skip to content

Commit

Permalink
Resolve OAuth2Error from WWW-Authenticate header
Browse files Browse the repository at this point in the history
Issue gh-7699
  • Loading branch information
jgrandja committed Feb 21, 2020
1 parent 69156b7 commit f2da2c5
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

package org.springframework.security.oauth2.client.web.reactive.function.client;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
Expand All @@ -34,20 +34,22 @@
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.web.RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.web.SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.web.SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
Expand All @@ -62,6 +64,8 @@
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the
Expand Down Expand Up @@ -614,32 +618,84 @@ private AuthorizationFailureForwarder(ReactiveOAuth2AuthorizationFailureHandler
}

@Override
public Mono<ClientResponse> handleResponse(
ClientRequest request,
Mono<ClientResponse> responseMono) {

public Mono<ClientResponse> handleResponse(ClientRequest request, Mono<ClientResponse> responseMono) {
return responseMono
.flatMap(response -> handleHttpStatus(request, response.rawStatusCode(), null)
.flatMap(response -> handleResponse(request, response)
.thenReturn(response))
.onErrorResume(WebClientResponseException.class, e -> handleHttpStatus(request, e.getRawStatusCode(), e)
.onErrorResume(WebClientResponseException.class, e -> handleWebClientResponseException(request, e)
.then(Mono.error(e)))
.onErrorResume(OAuth2AuthorizationException.class, e -> handleAuthorizationException(request, e)
.then(Mono.error(e)));
}

private Mono<Void> handleResponse(ClientRequest request, ClientResponse response) {
return Mono.justOrEmpty(resolveErrorIfPossible(response))
.flatMap(oauth2Error -> {
Mono<Optional<ServerWebExchange>> serverWebExchange = effectiveServerWebExchange(request);

Mono<String> clientRegistrationId = effectiveClientRegistrationId(request);

return Mono.zip(currentAuthenticationMono, serverWebExchange, clientRegistrationId)
.flatMap(tuple3 -> handleAuthorizationFailure(
tuple3.getT1(), // Authentication principal
tuple3.getT2().orElse(null), // ServerWebExchange exchange
new ClientAuthorizationException(
oauth2Error,
tuple3.getT3()))); // String clientRegistrationId
});
}

private OAuth2Error resolveErrorIfPossible(ClientResponse response) {
// Try to resolve from 'WWW-Authenticate' header
if (!response.headers().header(HttpHeaders.WWW_AUTHENTICATE).isEmpty()) {
String wwwAuthenticateHeader = response.headers().header(HttpHeaders.WWW_AUTHENTICATE).get(0);
Map<String, String> authParameters = parseAuthParameters(wwwAuthenticateHeader);
if (authParameters.containsKey(OAuth2ParameterNames.ERROR)) {
return new OAuth2Error(
authParameters.get(OAuth2ParameterNames.ERROR),
authParameters.get(OAuth2ParameterNames.ERROR_DESCRIPTION),
authParameters.get(OAuth2ParameterNames.ERROR_URI));
}
}
return resolveErrorIfPossible(response.rawStatusCode());
}

private OAuth2Error resolveErrorIfPossible(int statusCode) {
if (this.httpStatusToOAuth2ErrorCodeMap.containsKey(statusCode)) {
return new OAuth2Error(
this.httpStatusToOAuth2ErrorCodeMap.get(statusCode),
null,
"https://tools.ietf.org/html/rfc6750#section-3.1");
}
return null;
}

private Map<String, String> parseAuthParameters(String wwwAuthenticateHeader) {
return Stream.of(wwwAuthenticateHeader)
.filter(header -> !StringUtils.isEmpty(header))
.filter(header -> header.toLowerCase().startsWith("bearer"))
.map(header -> header.substring("bearer".length()))
.map(header -> header.split(","))
.flatMap(Stream::of)
.map(parameter -> parameter.split("="))
.filter(parameter -> parameter.length > 1)
.collect(Collectors.toMap(
parameters -> parameters[0].trim(),
parameters -> parameters[1].trim().replace("\"", "")));
}

/**
* Handles the given http status code returned from a resource server
* by notifying the authorization failure handler if the http status
* code is in the {@link #httpStatusToOAuth2ErrorCodeMap}.
*
* @param request the request being processed
* @param httpStatusCode the http status returned by the resource server
* @param exception The root cause exception for the failure (nullable)
* @param exception The root cause exception for the failure
* @return a {@link Mono} that completes empty after the authorization failure handler completes.
*/
private Mono<Void> handleHttpStatus(ClientRequest request, int httpStatusCode, @Nullable Exception exception) {
return Mono.justOrEmpty(this.httpStatusToOAuth2ErrorCodeMap.get(httpStatusCode))
.flatMap(oauth2ErrorCode -> {
private Mono<Void> handleWebClientResponseException(ClientRequest request, WebClientResponseException exception) {
return Mono.justOrEmpty(resolveErrorIfPossible(exception.getRawStatusCode()))
.flatMap(oauth2Error -> {
Mono<Optional<ServerWebExchange>> serverWebExchange = effectiveServerWebExchange(request);

Mono<String> clientRegistrationId = effectiveClientRegistrationId(request);
Expand All @@ -648,9 +704,9 @@ private Mono<Void> handleHttpStatus(ClientRequest request, int httpStatusCode, @
.flatMap(tuple3 -> handleAuthorizationFailure(
tuple3.getT1(), // Authentication principal
tuple3.getT2().orElse(null), // ServerWebExchange exchange
createAuthorizationException(
new ClientAuthorizationException(
oauth2Error,
tuple3.getT3(), // String clientRegistrationId
oauth2ErrorCode,
exception)));
});
}
Expand All @@ -673,28 +729,6 @@ private Mono<Void> handleAuthorizationException(ClientRequest request, OAuth2Aut
exception));
}

/**
* Creates an authorization exception using the given parameters.
*
* @param clientRegistrationId the client registration id of the client that failed authentication/authorization.
* @param oauth2ErrorCode the OAuth 2.0 error code to use in the authorization failure event
* @param exception The root cause exception for the failure (nullable)
* @return an authorization exception using the given parameters.
*/
private ClientAuthorizationException createAuthorizationException(
String clientRegistrationId,
String oauth2ErrorCode,
@Nullable Exception exception) {
return new ClientAuthorizationException(
new OAuth2Error(
oauth2ErrorCode,
null,
"https://tools.ietf.org/html/rfc6750#section-3.1"),
clientRegistrationId,
exception);
}


/**
* Delegates to the authorization failure handler of the failed authorization.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.server.ServerWebExchange;
Expand All @@ -98,6 +99,7 @@
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -173,6 +175,7 @@ public void setup() {
this.authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
when(this.exchange.getResponse().headers()).thenReturn(mock(ClientResponse.Headers.class));
}

@Test
Expand Down Expand Up @@ -621,6 +624,54 @@ public void filterWhenForbiddenWithWebClientExceptionThenInvokeFailureHandler()
.containsExactly(entry(ServerWebExchange.class.getName(), this.serverWebExchange));
}

@Test
public void filterWhenWWWAuthenticateHeaderIncludesErrorThenInvokeFailureHandler() {
function.setAuthorizationFailureHandler(authorizationFailureHandler);

PublisherProbe<Void> publisherProbe = PublisherProbe.empty();
when(authorizationFailureHandler.onAuthorizationFailure(any(), any(), any())).thenReturn(publisherProbe.mono());

OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt());
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
"principalName", this.accessToken, refreshToken);
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(oauth2AuthorizedClient(authorizedClient))
.build();

String wwwAuthenticateHeader = "Bearer error=\"insufficient_scope\", " +
"error_description=\"The request requires higher privileges than provided by the access token.\", " +
"error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"";
ClientResponse.Headers headers = mock(ClientResponse.Headers.class);
when(headers.header(eq(HttpHeaders.WWW_AUTHENTICATE)))
.thenReturn(Collections.singletonList(wwwAuthenticateHeader));
when(this.exchange.getResponse().headers()).thenReturn(headers);

this.function.filter(request, this.exchange)
.subscriberContext(serverWebExchange())
.block();

assertThat(publisherProbe.wasSubscribed()).isTrue();

verify(authorizationFailureHandler).onAuthorizationFailure(
authorizationExceptionCaptor.capture(),
authenticationCaptor.capture(),
attributesCaptor.capture());

assertThat(authorizationExceptionCaptor.getValue())
.isInstanceOfSatisfying(ClientAuthorizationException.class, e -> {
assertThat(e.getClientRegistrationId()).isEqualTo(registration.getRegistrationId());
assertThat(e.getError().getErrorCode()).isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
assertThat(e.getError().getDescription()).isEqualTo("The request requires higher privileges than provided by the access token.");
assertThat(e.getError().getUri()).isEqualTo("https://tools.ietf.org/html/rfc6750#section-3.1");
assertThat(e).hasNoCause();
assertThat(e).hasMessageContaining(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
});
assertThat(authenticationCaptor.getValue())
.isInstanceOf(AnonymousAuthenticationToken.class);
assertThat(attributesCaptor.getValue())
.containsExactly(entry(ServerWebExchange.class.getName(), this.serverWebExchange));
}

@Test
public void filterWhenAuthorizationExceptionThenInvokeFailureHandler() {
function.setAuthorizationFailureHandler(authorizationFailureHandler);
Expand Down

0 comments on commit f2da2c5

Please sign in to comment.