|
@@ -1,5 +1,5 @@
|
|
/*
|
|
/*
|
|
- * Copyright 2002-2019 the original author or authors.
|
|
|
|
|
|
+ * Copyright 2002-2020 the original author or authors.
|
|
*
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* you may not use this file except in compliance with the License.
|
|
@@ -16,19 +16,26 @@
|
|
|
|
|
|
package org.springframework.security.oauth2.client.web.reactive.function.client;
|
|
package org.springframework.security.oauth2.client.web.reactive.function.client;
|
|
|
|
|
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
|
+import org.springframework.lang.Nullable;
|
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.authority.AuthorityUtils;
|
|
import org.springframework.security.core.authority.AuthorityUtils;
|
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
|
import org.springframework.security.core.context.SecurityContext;
|
|
import org.springframework.security.core.context.SecurityContext;
|
|
|
|
+import org.springframework.security.oauth2.client.ClientAuthorizationException;
|
|
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
|
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
|
|
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
|
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
|
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
|
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
|
|
|
+import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizationFailureHandler;
|
|
|
|
+import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizationSuccessHandler;
|
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
|
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
|
|
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
|
|
import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider;
|
|
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.authentication.OAuth2AuthenticationToken;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
|
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
|
@@ -37,15 +44,21 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
|
|
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
|
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
|
|
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
|
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
|
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
|
|
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.util.Assert;
|
|
import org.springframework.util.Assert;
|
|
import org.springframework.web.reactive.function.client.ClientRequest;
|
|
import org.springframework.web.reactive.function.client.ClientRequest;
|
|
import org.springframework.web.reactive.function.client.ClientResponse;
|
|
import org.springframework.web.reactive.function.client.ClientResponse;
|
|
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
|
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
|
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
|
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
|
|
|
+import org.springframework.web.reactive.function.client.WebClientResponseException;
|
|
import org.springframework.web.server.ServerWebExchange;
|
|
import org.springframework.web.server.ServerWebExchange;
|
|
import reactor.core.publisher.Mono;
|
|
import reactor.core.publisher.Mono;
|
|
|
|
|
|
import java.time.Duration;
|
|
import java.time.Duration;
|
|
|
|
+import java.util.Collections;
|
|
|
|
+import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Optional;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Consumer;
|
|
@@ -54,8 +67,27 @@ import java.util.function.Consumer;
|
|
* Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the
|
|
* Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the
|
|
* token as a Bearer Token.
|
|
* token as a Bearer Token.
|
|
*
|
|
*
|
|
|
|
+ * <h3>Authentication and Authorization Failures</h3>
|
|
|
|
+ *
|
|
|
|
+ * <p>Since 5.3, this filter function has the ability to forward authentication (HTTP 401 Unauthorized)
|
|
|
|
+ * and authorization (HTTP 403 Forbidden) failures from an OAuth 2.0 Resource Server to a
|
|
|
|
+ * {@link ReactiveOAuth2AuthorizationFailureHandler}.
|
|
|
|
+ * A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} can be used
|
|
|
|
+ * to remove the cached {@link OAuth2AuthorizedClient}, so that future requests will result
|
|
|
|
+ * in a new token being retrieved from an Authorization Server, and sent to the Resource Server.</p>
|
|
|
|
+ *
|
|
|
|
+ * <p>If the {@link #ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveClientRegistrationRepository, ServerOAuth2AuthorizedClientRepository)}
|
|
|
|
+ * constructor is used, a {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
|
|
|
|
+ * will be configured automatically.</p>
|
|
|
|
+ *
|
|
|
|
+ * <p>If the {@link #ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)}
|
|
|
|
+ * constructor is used, a {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
|
|
|
|
+ * will <em>NOT</em> be configured automatically.
|
|
|
|
+ * It is recommended that you configure one via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.</p>
|
|
|
|
+ *
|
|
* @author Rob Winch
|
|
* @author Rob Winch
|
|
* @author Joe Grandja
|
|
* @author Joe Grandja
|
|
|
|
+ * @author Phil Clay
|
|
* @since 5.1
|
|
* @since 5.1
|
|
*/
|
|
*/
|
|
public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
|
|
public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
|
|
@@ -77,7 +109,20 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
private static final AnonymousAuthenticationToken ANONYMOUS_USER_TOKEN = new AnonymousAuthenticationToken("anonymous", "anonymousUser",
|
|
private static final AnonymousAuthenticationToken ANONYMOUS_USER_TOKEN = new AnonymousAuthenticationToken("anonymous", "anonymousUser",
|
|
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
|
AuthorityUtils.createAuthorityList("ROLE_USER"));
|
|
|
|
|
|
- private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
|
|
|
|
|
+ private final Mono<Authentication> currentAuthenticationMono = ReactiveSecurityContextHolder.getContext()
|
|
|
|
+ .map(SecurityContext::getAuthentication)
|
|
|
|
+ .defaultIfEmpty(ANONYMOUS_USER_TOKEN);
|
|
|
|
+
|
|
|
|
+ private final Mono<String> clientRegistrationIdMono = currentAuthenticationMono
|
|
|
|
+ .filter(t -> this.defaultOAuth2AuthorizedClient && t instanceof OAuth2AuthenticationToken)
|
|
|
|
+ .cast(OAuth2AuthenticationToken.class)
|
|
|
|
+ .map(OAuth2AuthenticationToken::getAuthorizedClientRegistrationId);
|
|
|
|
+
|
|
|
|
+ private final Mono<ServerWebExchange> currentServerWebExchangeMono = Mono.subscriberContext()
|
|
|
|
+ .filter(c -> c.hasKey(ServerWebExchange.class))
|
|
|
|
+ .map(c -> c.get(ServerWebExchange.class));
|
|
|
|
+
|
|
|
|
+ private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
|
|
|
|
|
|
private boolean defaultAuthorizedClientManager;
|
|
private boolean defaultAuthorizedClientManager;
|
|
|
|
|
|
@@ -91,33 +136,71 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
@Deprecated
|
|
@Deprecated
|
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient;
|
|
private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient;
|
|
|
|
|
|
|
|
+ private ClientResponseHandler clientResponseHandler;
|
|
|
|
+
|
|
|
|
+ @FunctionalInterface
|
|
|
|
+ private interface ClientResponseHandler {
|
|
|
|
+ Mono<ClientResponse> handleResponse(ClientRequest request, Mono<ClientResponse> response);
|
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
/**
|
|
* Constructs a {@code ServerOAuth2AuthorizedClientExchangeFilterFunction} using the provided parameters.
|
|
* Constructs a {@code ServerOAuth2AuthorizedClientExchangeFilterFunction} using the provided parameters.
|
|
*
|
|
*
|
|
|
|
+ * <p>When this constructor is used, authentication (HTTP 401) and authorization (HTTP 403)
|
|
|
|
+ * failures returned from a OAuth 2.0 Resource Server will <em>NOT</em> be forwarded to a
|
|
|
|
+ * {@link ReactiveOAuth2AuthorizationFailureHandler}.
|
|
|
|
+ * Therefore, future requests to the Resource Server will most likely use the same (most likely invalid) token,
|
|
|
|
+ * resulting in the same errors returned from the Resource Server.
|
|
|
|
+ * It is recommended to configure a {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
|
|
|
|
+ * via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}
|
|
|
|
+ * so that authentication and authorization failures returned from a Resource Server
|
|
|
|
+ * will result in removing the authorized client, so that a new token is retrieved for future requests.</p>
|
|
|
|
+ *
|
|
* @since 5.2
|
|
* @since 5.2
|
|
* @param authorizedClientManager the {@link ReactiveOAuth2AuthorizedClientManager} which manages the authorized client(s)
|
|
* @param authorizedClientManager the {@link ReactiveOAuth2AuthorizedClientManager} which manages the authorized client(s)
|
|
*/
|
|
*/
|
|
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
|
|
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
|
|
Assert.notNull(authorizedClientManager, "authorizedClientManager cannot be null");
|
|
Assert.notNull(authorizedClientManager, "authorizedClientManager cannot be null");
|
|
this.authorizedClientManager = authorizedClientManager;
|
|
this.authorizedClientManager = authorizedClientManager;
|
|
|
|
+ this.clientResponseHandler = (request, responseMono) -> responseMono;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
* Constructs a {@code ServerOAuth2AuthorizedClientExchangeFilterFunction} using the provided parameters.
|
|
* Constructs a {@code ServerOAuth2AuthorizedClientExchangeFilterFunction} using the provided parameters.
|
|
*
|
|
*
|
|
|
|
+ * <p>Since 5.3, when this constructor is used, authentication (HTTP 401)
|
|
|
|
+ * and authorization (HTTP 403) failures returned from an OAuth 2.0 Resource Server
|
|
|
|
+ * will be forwarded to a {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler},
|
|
|
|
+ * which will potentially remove the {@link OAuth2AuthorizedClient} from the given
|
|
|
|
+ * {@link ServerOAuth2AuthorizedClientRepository}, depending on the OAuth 2.0 error code returned.
|
|
|
|
+ * Authentication failures returned from an OAuth 2.0 Resource Server typically indicate
|
|
|
|
+ * that the token is invalid, and should not be used in future requests.
|
|
|
|
+ * Removing the authorized client from the repository will ensure that the existing
|
|
|
|
+ * token will not be sent for future requests to the Resource Server,
|
|
|
|
+ * and a new token is retrieved from Authorization Server and used for
|
|
|
|
+ * future requests to the Resource Server.</p>
|
|
|
|
+ *
|
|
* @param clientRegistrationRepository the repository of client registrations
|
|
* @param clientRegistrationRepository the repository of client registrations
|
|
* @param authorizedClientRepository the repository of authorized clients
|
|
* @param authorizedClientRepository the repository of authorized clients
|
|
*/
|
|
*/
|
|
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
|
|
public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
|
|
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
|
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
|
- this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
|
|
|
|
|
|
+
|
|
|
|
+ ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler =
|
|
|
|
+ new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(authorizedClientRepository);
|
|
|
|
+
|
|
|
|
+ this.authorizedClientManager = createDefaultAuthorizedClientManager(
|
|
|
|
+ clientRegistrationRepository,
|
|
|
|
+ authorizedClientRepository,
|
|
|
|
+ authorizationFailureHandler);
|
|
|
|
+ this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler);
|
|
this.defaultAuthorizedClientManager = true;
|
|
this.defaultAuthorizedClientManager = true;
|
|
}
|
|
}
|
|
|
|
|
|
private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager(
|
|
private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager(
|
|
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
|
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
|
- ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
|
|
|
|
|
+ ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
|
|
|
|
+ ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
|
|
|
|
|
|
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
|
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
|
|
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
|
|
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
|
|
@@ -132,7 +215,8 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager =
|
|
UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager =
|
|
new UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
|
new UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
|
clientRegistrationRepository,
|
|
clientRegistrationRepository,
|
|
- (UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository);
|
|
|
|
|
|
+ (UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository,
|
|
|
|
+ authorizationFailureHandler);
|
|
unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
|
unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
|
return unauthenticatedAuthorizedClientManager;
|
|
return unauthenticatedAuthorizedClientManager;
|
|
}
|
|
}
|
|
@@ -140,6 +224,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
|
|
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
|
|
clientRegistrationRepository, authorizedClientRepository);
|
|
clientRegistrationRepository, authorizedClientRepository);
|
|
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
|
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
|
|
|
+ authorizedClientManager.setAuthorizationFailureHandler(authorizationFailureHandler);
|
|
|
|
|
|
return authorizedClientManager;
|
|
return authorizedClientManager;
|
|
}
|
|
}
|
|
@@ -316,8 +401,13 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
|
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
|
return authorizedClient(request)
|
|
return authorizedClient(request)
|
|
.map(authorizedClient -> bearer(request, authorizedClient))
|
|
.map(authorizedClient -> bearer(request, authorizedClient))
|
|
- .flatMap(next::exchange)
|
|
|
|
- .switchIfEmpty(Mono.defer(() -> next.exchange(request)));
|
|
|
|
|
|
+ .flatMap(requestWithBearer -> exchangeAndHandleResponse(requestWithBearer, next))
|
|
|
|
+ .switchIfEmpty(Mono.defer(() -> exchangeAndHandleResponse(request, next)));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private Mono<ClientResponse> exchangeAndHandleResponse(ClientRequest request, ExchangeFunction next) {
|
|
|
|
+ return next.exchange(request)
|
|
|
|
+ .transform(responseMono -> this.clientResponseHandler.handleResponse(request, responseMono));
|
|
}
|
|
}
|
|
|
|
|
|
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request) {
|
|
private Mono<OAuth2AuthorizedClient> authorizedClient(ClientRequest request) {
|
|
@@ -330,80 +420,102 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
}
|
|
}
|
|
|
|
|
|
private Mono<OAuth2AuthorizeRequest> authorizeRequest(ClientRequest request) {
|
|
private Mono<OAuth2AuthorizeRequest> authorizeRequest(ClientRequest request) {
|
|
- Mono<Authentication> authentication = currentAuthentication();
|
|
|
|
-
|
|
|
|
- Mono<String> clientRegistrationId = Mono.justOrEmpty(clientRegistrationId(request))
|
|
|
|
- .switchIfEmpty(Mono.justOrEmpty(this.defaultClientRegistrationId))
|
|
|
|
- .switchIfEmpty(clientRegistrationId(authentication));
|
|
|
|
|
|
+ Mono<String> clientRegistrationId = effectiveClientRegistrationId(request);
|
|
|
|
|
|
- Mono<Optional<ServerWebExchange>> serverWebExchange = Mono.justOrEmpty(serverWebExchange(request))
|
|
|
|
- .switchIfEmpty(currentServerWebExchange())
|
|
|
|
- .map(Optional::of)
|
|
|
|
- .defaultIfEmpty(Optional.empty());
|
|
|
|
|
|
+ Mono<Optional<ServerWebExchange>> serverWebExchange = effectiveServerWebExchange(request);
|
|
|
|
|
|
- return Mono.zip(clientRegistrationId, authentication, serverWebExchange)
|
|
|
|
|
|
+ return Mono.zip(clientRegistrationId, this.currentAuthenticationMono, serverWebExchange)
|
|
.map(t3 -> {
|
|
.map(t3 -> {
|
|
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1()).principal(t3.getT2());
|
|
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1()).principal(t3.getT2());
|
|
- if (t3.getT3().isPresent()) {
|
|
|
|
- builder.attribute(ServerWebExchange.class.getName(), t3.getT3().get());
|
|
|
|
- }
|
|
|
|
|
|
+ t3.getT3().ifPresent(exchange -> builder.attribute(ServerWebExchange.class.getName(), exchange));
|
|
return builder.build();
|
|
return builder.build();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
- private Mono<OAuth2AuthorizeRequest> reauthorizeRequest(ClientRequest request, OAuth2AuthorizedClient authorizedClient) {
|
|
|
|
- Mono<Authentication> authentication = currentAuthentication();
|
|
|
|
|
|
+ /**
|
|
|
|
+ * Returns a {@link Mono} the emits the {@code clientRegistrationId}
|
|
|
|
+ * that is active for the given request.
|
|
|
|
+ *
|
|
|
|
+ * @param request the request for which to retrieve the {@code clientRegistrationId}
|
|
|
|
+ * @return a mono that emits the {@code clientRegistrationId}
|
|
|
|
+ * that is active for the given request.
|
|
|
|
+ */
|
|
|
|
+ private Mono<String> effectiveClientRegistrationId(ClientRequest request) {
|
|
|
|
+ return Mono.justOrEmpty(clientRegistrationId(request))
|
|
|
|
+ .switchIfEmpty(Mono.justOrEmpty(this.defaultClientRegistrationId))
|
|
|
|
+ .switchIfEmpty(clientRegistrationIdMono);
|
|
|
|
+ }
|
|
|
|
|
|
- Mono<Optional<ServerWebExchange>> serverWebExchange = Mono.justOrEmpty(serverWebExchange(request))
|
|
|
|
- .switchIfEmpty(currentServerWebExchange())
|
|
|
|
|
|
+ /**
|
|
|
|
+ * Returns a {@link Mono} that emits an {@link Optional} for the {@link ServerWebExchange}
|
|
|
|
+ * that is active for the given request.
|
|
|
|
+ *
|
|
|
|
+ * <p>The returned {@link Mono} will never complete empty.
|
|
|
|
+ * Instead, it will emit an empty {@link Optional} if no exchange is active.</p>
|
|
|
|
+ *
|
|
|
|
+ * @param request the request for which to retrieve the exchange
|
|
|
|
+ * @return a {@link Mono} that emits an {@link Optional} for the {@link ServerWebExchange}
|
|
|
|
+ * that is active for the given request.
|
|
|
|
+ */
|
|
|
|
+ private Mono<Optional<ServerWebExchange>> effectiveServerWebExchange(ClientRequest request) {
|
|
|
|
+ return Mono.justOrEmpty(serverWebExchange(request))
|
|
|
|
+ .switchIfEmpty(currentServerWebExchangeMono)
|
|
.map(Optional::of)
|
|
.map(Optional::of)
|
|
.defaultIfEmpty(Optional.empty());
|
|
.defaultIfEmpty(Optional.empty());
|
|
|
|
+ }
|
|
|
|
|
|
- return Mono.zip(authentication, serverWebExchange)
|
|
|
|
|
|
+ private Mono<OAuth2AuthorizeRequest> reauthorizeRequest(ClientRequest request, OAuth2AuthorizedClient authorizedClient) {
|
|
|
|
+ Mono<Optional<ServerWebExchange>> serverWebExchange = effectiveServerWebExchange(request);
|
|
|
|
+
|
|
|
|
+ return Mono.zip(this.currentAuthenticationMono, serverWebExchange)
|
|
.map(t2 -> {
|
|
.map(t2 -> {
|
|
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withAuthorizedClient(authorizedClient).principal(t2.getT1());
|
|
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withAuthorizedClient(authorizedClient).principal(t2.getT1());
|
|
- if (t2.getT2().isPresent()) {
|
|
|
|
- builder.attribute(ServerWebExchange.class.getName(), t2.getT2().get());
|
|
|
|
- }
|
|
|
|
|
|
+ t2.getT2().ifPresent(exchange -> builder.attribute(ServerWebExchange.class.getName(), exchange));
|
|
return builder.build();
|
|
return builder.build();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
- private Mono<Authentication> currentAuthentication() {
|
|
|
|
- return ReactiveSecurityContextHolder.getContext()
|
|
|
|
- .map(SecurityContext::getAuthentication)
|
|
|
|
- .defaultIfEmpty(ANONYMOUS_USER_TOKEN);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private Mono<String> clientRegistrationId(Mono<Authentication> authentication) {
|
|
|
|
- return authentication
|
|
|
|
- .filter(t -> this.defaultOAuth2AuthorizedClient && t instanceof OAuth2AuthenticationToken)
|
|
|
|
- .cast(OAuth2AuthenticationToken.class)
|
|
|
|
- .map(OAuth2AuthenticationToken::getAuthorizedClientRegistrationId);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private Mono<ServerWebExchange> currentServerWebExchange() {
|
|
|
|
- return Mono.subscriberContext()
|
|
|
|
- .filter(c -> c.hasKey(ServerWebExchange.class))
|
|
|
|
- .map(c -> c.get(ServerWebExchange.class));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) {
|
|
private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) {
|
|
return ClientRequest.from(request)
|
|
return ClientRequest.from(request)
|
|
.headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
|
|
.headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
|
|
.build();
|
|
.build();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * Sets the handler that handles authentication and authorization failures when communicating
|
|
|
|
+ * to the OAuth 2.0 Resource Server.
|
|
|
|
+ *
|
|
|
|
+ * <p>For example, a {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
|
|
|
|
+ * is typically used to remove the cached {@link OAuth2AuthorizedClient},
|
|
|
|
+ * so that the same token is no longer used in future requests to the Resource Server.</p>
|
|
|
|
+ *
|
|
|
|
+ * <p>The failure handler used by default depends on which constructor was used
|
|
|
|
+ * to construct this {@link ServerOAuth2AuthorizedClientExchangeFilterFunction}.
|
|
|
|
+ * See the constructors for more details.</p>
|
|
|
|
+ *
|
|
|
|
+ * @param authorizationFailureHandler the handler that handles authentication and authorization failures.
|
|
|
|
+ * @since 5.3
|
|
|
|
+ */
|
|
|
|
+ public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
|
|
|
|
+ Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
|
|
|
|
+ this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler);
|
|
|
|
+ }
|
|
|
|
+
|
|
private static class UnAuthenticatedReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
|
|
private static class UnAuthenticatedReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
|
|
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
|
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
|
|
private final UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
|
private final UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository;
|
|
|
|
+ private final ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
|
|
|
|
+ private final ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
|
|
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
|
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
|
|
|
|
|
|
private UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
|
private UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
|
|
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
|
ReactiveClientRegistrationRepository clientRegistrationRepository,
|
|
- UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
|
|
|
|
|
|
+ UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository,
|
|
|
|
+ ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
|
|
this.clientRegistrationRepository = clientRegistrationRepository;
|
|
this.clientRegistrationRepository = clientRegistrationRepository;
|
|
this.authorizedClientRepository = authorizedClientRepository;
|
|
this.authorizedClientRepository = authorizedClientRepository;
|
|
|
|
+ this.authorizationSuccessHandler = new SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler(authorizedClientRepository);
|
|
|
|
+ this.authorizationFailureHandler = authorizationFailureHandler;
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -418,8 +530,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
.flatMap(authorizedClient -> {
|
|
.flatMap(authorizedClient -> {
|
|
// Re-authorize
|
|
// Re-authorize
|
|
return Mono.just(OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient).principal(principal).build())
|
|
return Mono.just(OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient).principal(principal).build())
|
|
- .flatMap(this.authorizedClientProvider::authorize)
|
|
|
|
- .flatMap(reauthorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(reauthorizedClient, principal, null).thenReturn(reauthorizedClient))
|
|
|
|
|
|
+ .flatMap(authorizationContext -> authorize(authorizationContext, principal))
|
|
// Default to the existing authorizedClient if the client was not re-authorized
|
|
// Default to the existing authorizedClient if the client was not re-authorized
|
|
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
|
|
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
|
|
authorizeRequest.getAuthorizedClient() : authorizedClient);
|
|
authorizeRequest.getAuthorizedClient() : authorizedClient);
|
|
@@ -430,15 +541,184 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
|
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
|
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
|
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
|
|
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
|
|
.flatMap(clientRegistration -> Mono.just(OAuth2AuthorizationContext.withClientRegistration(clientRegistration).principal(principal).build()))
|
|
.flatMap(clientRegistration -> Mono.just(OAuth2AuthorizationContext.withClientRegistration(clientRegistration).principal(principal).build()))
|
|
- .flatMap(this.authorizedClientProvider::authorize)
|
|
|
|
- .flatMap(authorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, null).thenReturn(authorizedClient))
|
|
|
|
|
|
+ .flatMap(authorizationContext -> authorize(authorizationContext, principal))
|
|
.subscriberContext(context)
|
|
.subscriberContext(context)
|
|
));
|
|
));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * Performs authorization and then delegates to either the {@link #authorizationSuccessHandler}
|
|
|
|
+ * or {@link #authorizationFailureHandler}, depending on the authorization result.
|
|
|
|
+ *
|
|
|
|
+ * @param authorizationContext the context to authorize
|
|
|
|
+ * @param principal the principle to authorize
|
|
|
|
+ * @return a {@link Mono} that emits the authorized client after the authorization attempt succeeds
|
|
|
|
+ * and the {@link #authorizationSuccessHandler} has completed,
|
|
|
|
+ * or completes with an exception after the authorization attempt fails
|
|
|
|
+ * and the {@link #authorizationFailureHandler} has completed
|
|
|
|
+ */
|
|
|
|
+ private Mono<OAuth2AuthorizedClient> authorize(
|
|
|
|
+ OAuth2AuthorizationContext authorizationContext,
|
|
|
|
+ Authentication principal) {
|
|
|
|
+
|
|
|
|
+ return this.authorizedClientProvider.authorize(authorizationContext)
|
|
|
|
+ // Delegates to the authorizationSuccessHandler of the successful authorization
|
|
|
|
+ .flatMap(authorizedClient -> this.authorizationSuccessHandler.onAuthorizationSuccess(
|
|
|
|
+ authorizedClient,
|
|
|
|
+ principal,
|
|
|
|
+ Collections.emptyMap())
|
|
|
|
+ .thenReturn(authorizedClient))
|
|
|
|
+ // Delegates to the authorizationFailureHandler of the failed authorization
|
|
|
|
+ .onErrorResume(OAuth2AuthorizationException.class, authorizationException -> this.authorizationFailureHandler.onAuthorizationFailure(
|
|
|
|
+ authorizationException,
|
|
|
|
+ principal,
|
|
|
|
+ Collections.emptyMap())
|
|
|
|
+ .then(Mono.error(authorizationException)));
|
|
|
|
+ }
|
|
|
|
+
|
|
private void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
|
private void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
|
|
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
|
|
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
|
|
this.authorizedClientProvider = authorizedClientProvider;
|
|
this.authorizedClientProvider = authorizedClientProvider;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Forwards authentication and authorization failures to a
|
|
|
|
+ * {@link ReactiveOAuth2AuthorizationFailureHandler}.
|
|
|
|
+ *
|
|
|
|
+ * @since 5.3
|
|
|
|
+ */
|
|
|
|
+ private class AuthorizationFailureForwarder implements ClientResponseHandler {
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * A map of HTTP Status Code to OAuth 2.0 Error codes for
|
|
|
|
+ * HTTP status codes that should be interpreted as
|
|
|
|
+ * authentication or authorization failures.
|
|
|
|
+ */
|
|
|
|
+ private final Map<Integer, String> httpStatusToOAuth2ErrorCodeMap;
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * The {@link ReactiveOAuth2AuthorizationFailureHandler} to notify
|
|
|
|
+ * when an authentication/authorization failure occurs.
|
|
|
|
+ */
|
|
|
|
+ private final ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
|
|
|
|
+
|
|
|
|
+ private AuthorizationFailureForwarder(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
|
|
|
|
+ Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
|
|
|
|
+ this.authorizationFailureHandler = authorizationFailureHandler;
|
|
|
|
+
|
|
|
|
+ Map<Integer, String> httpStatusToOAuth2Error = new HashMap<>();
|
|
|
|
+ httpStatusToOAuth2Error.put(HttpStatus.UNAUTHORIZED.value(), OAuth2ErrorCodes.INVALID_TOKEN);
|
|
|
|
+ httpStatusToOAuth2Error.put(HttpStatus.FORBIDDEN.value(), OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
|
|
|
+ this.httpStatusToOAuth2ErrorCodeMap = Collections.unmodifiableMap(httpStatusToOAuth2Error);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public Mono<ClientResponse> handleResponse(
|
|
|
|
+ ClientRequest request,
|
|
|
|
+ Mono<ClientResponse> responseMono) {
|
|
|
|
+
|
|
|
|
+ return responseMono
|
|
|
|
+ .flatMap(response -> handleHttpStatus(request, response.rawStatusCode(), null)
|
|
|
|
+ .thenReturn(response))
|
|
|
|
+ .onErrorResume(WebClientResponseException.class, e -> handleHttpStatus(request, e.getRawStatusCode(), e)
|
|
|
|
+ .then(Mono.error(e)))
|
|
|
|
+ .onErrorResume(OAuth2AuthorizationException.class, e -> handleAuthorizationException(request, e)
|
|
|
|
+ .then(Mono.error(e)));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 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)
|
|
|
|
+ * @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 -> {
|
|
|
|
+ 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
|
|
|
|
+ createAuthorizationException(
|
|
|
|
+ tuple3.getT3(), // String clientRegistrationId
|
|
|
|
+ oauth2ErrorCode,
|
|
|
|
+ exception)));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Handles the given OAuth2AuthorizationException that occurred downstream
|
|
|
|
+ * by notifying the authorization failure handler.
|
|
|
|
+ *
|
|
|
|
+ * @param request the request being processed
|
|
|
|
+ * @param exception the authorization exception to include in the failure event.
|
|
|
|
+ * @return a {@link Mono} that completes empty after the authorization failure handler completes.
|
|
|
|
+ */
|
|
|
|
+ private Mono<Void> handleAuthorizationException(ClientRequest request, OAuth2AuthorizationException exception) {
|
|
|
|
+ Mono<Optional<ServerWebExchange>> serverWebExchange = effectiveServerWebExchange(request);
|
|
|
|
+
|
|
|
|
+ return Mono.zip(currentAuthenticationMono, serverWebExchange)
|
|
|
|
+ .flatMap(tuple2 -> handleAuthorizationFailure(
|
|
|
|
+ tuple2.getT1(), // Authentication principal
|
|
|
|
+ tuple2.getT2().orElse(null), // ServerWebExchange exchange
|
|
|
|
+ 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.
|
|
|
|
+ *
|
|
|
|
+ * @param principal the principal associated with the failed authorization attempt
|
|
|
|
+ * @param exchange the currently active exchange
|
|
|
|
+ * @param exception the authorization exception to include in the failure event.
|
|
|
|
+ * @return a {@link Mono} that completes empty after the authorization failure handler completes.
|
|
|
|
+ */
|
|
|
|
+ private Mono<Void> handleAuthorizationFailure(
|
|
|
|
+ Authentication principal,
|
|
|
|
+ ServerWebExchange exchange,
|
|
|
|
+ OAuth2AuthorizationException exception) {
|
|
|
|
+
|
|
|
|
+ return this.authorizationFailureHandler.onAuthorizationFailure(
|
|
|
|
+ exception,
|
|
|
|
+ principal,
|
|
|
|
+ createAttributes(exchange));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private Map<String, Object> createAttributes(ServerWebExchange exchange) {
|
|
|
|
+ if (exchange == null) {
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
+ }
|
|
|
|
+ return Collections.singletonMap(ServerWebExchange.class.getName(), exchange);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|