org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager Maven / Gradle / Ivy
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
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.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* The default implementation of a {@link ReactiveOAuth2AuthorizedClientManager} for use
* within the context of a {@link ServerWebExchange}.
*
*
* (When operating outside of the context of a {@link ServerWebExchange}, use
* {@link org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
* AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} instead.)
*
*
*
* This is a reactive equivalent of {@link DefaultOAuth2AuthorizedClientManager}.
*
*
* Authorized Client Persistence
*
*
* This client manager utilizes a {@link ServerOAuth2AuthorizedClientRepository} to
* persist {@link OAuth2AuthorizedClient}s.
*
*
*
* By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
* will be saved in the authorized client repository. This functionality can be changed by
* configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler} via
* {@link #setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)}.
*
*
*
* By default, when an authorization attempt fails due to an
* {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error,
* the previously saved {@link OAuth2AuthorizedClient} will be removed from the authorized
* client repository. (The
* {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error
* generally occurs when a refresh token that is no longer valid is used to retrieve a new
* access token.) This functionality can be changed by configuring a custom
* {@link ReactiveOAuth2AuthorizationFailureHandler} via
* {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.
*
*
* @author Joe Grandja
* @author Phil Clay
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientManager
* @see ReactiveOAuth2AuthorizedClientProvider
* @see ReactiveOAuth2AuthorizationSuccessHandler
* @see ReactiveOAuth2AuthorizationFailureHandler
*/
public final class DefaultReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
// @formatter:off
private static final ReactiveOAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
// @formatter:on
// @formatter:off
private static final Mono currentServerWebExchangeMono = Mono.deferContextual(Mono::just)
.filter((c) -> c.hasKey(ServerWebExchange.class))
.map((c) -> c.get(ServerWebExchange.class));
// @formatter:on
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER;
private Function>> contextAttributesMapper = new DefaultContextAttributesMapper();
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
/**
* Constructs a {@code DefaultReactiveOAuth2AuthorizedClientManager} using the
* provided parameters.
* @param clientRegistrationRepository the repository of client registrations
* @param authorizedClientRepository the repository of authorized clients
*/
public DefaultReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientRepository = authorizedClientRepository;
this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientRepository
.saveAuthorizedClient(authorizedClient, principal,
(ServerWebExchange) attributes.get(ServerWebExchange.class.getName()));
this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> authorizedClientRepository.removeAuthorizedClient(
clientRegistrationId, principal,
(ServerWebExchange) attributes.get(ServerWebExchange.class.getName())));
}
@Override
public Mono authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
Authentication principal = authorizeRequest.getPrincipal();
// @formatter:off
return Mono.justOrEmpty(authorizeRequest.getAttribute(ServerWebExchange.class.getName()))
.switchIfEmpty(currentServerWebExchangeMono)
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
.flatMap((serverWebExchange) -> Mono
.justOrEmpty(authorizeRequest.getAuthorizedClient())
.switchIfEmpty(Mono.defer(() -> loadAuthorizedClient(clientRegistrationId, principal, serverWebExchange)))
.flatMap((authorizedClient) -> // Re-authorize
authorizationContext(authorizeRequest, authorizedClient)
.flatMap((authorizationContext) -> authorize(authorizationContext, principal, serverWebExchange))
// Default to the existing authorizedClient if the
// client was not re-authorized
.defaultIfEmpty((authorizeRequest.getAuthorizedClient() != null)
? authorizeRequest.getAuthorizedClient() : authorizedClient)
)
.switchIfEmpty(Mono.defer(() ->
// Authorize
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
.flatMap((clientRegistration) -> authorizationContext(authorizeRequest,
clientRegistration))
.flatMap((authorizationContext) -> authorize(authorizationContext, principal,
serverWebExchange))))
);
// @formatter:on
}
private Mono loadAuthorizedClient(String clientRegistrationId, Authentication principal,
ServerWebExchange serverWebExchange) {
return this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, serverWebExchange);
}
/**
* 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
* @param serverWebExchange the currently active exchange
* @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 authorize(OAuth2AuthorizationContext authorizationContext,
Authentication principal, ServerWebExchange serverWebExchange) {
// @formatter:off
return this.authorizedClientProvider.authorize(authorizationContext)
// Delegate to the authorizationSuccessHandler of the successful
// authorization
.flatMap((authorizedClient) ->
this.authorizationSuccessHandler
.onAuthorizationSuccess(authorizedClient, principal, createAttributes(serverWebExchange))
.thenReturn(authorizedClient)
)
// Delegate to the authorizationFailureHandler of the failed authorization
.onErrorResume(OAuth2AuthorizationException.class, (authorizationException) ->
this.authorizationFailureHandler
.onAuthorizationFailure(authorizationException, principal, createAttributes(serverWebExchange))
.then(Mono.error(authorizationException))
);
// @formatter:on
}
private Map createAttributes(ServerWebExchange serverWebExchange) {
return Collections.singletonMap(ServerWebExchange.class.getName(), serverWebExchange);
}
private Mono authorizationContext(OAuth2AuthorizeRequest authorizeRequest,
OAuth2AuthorizedClient authorizedClient) {
// @formatter:off
return Mono.just(authorizeRequest)
.flatMap(this.contextAttributesMapper)
.map((attrs) -> OAuth2AuthorizationContext
.withAuthorizedClient(authorizedClient)
.principal(authorizeRequest.getPrincipal())
.attributes((attributes) -> {
if (!CollectionUtils.isEmpty(attrs)) {
attributes.putAll(attrs);
}
})
.build());
// @formatter:on
}
private Mono authorizationContext(OAuth2AuthorizeRequest authorizeRequest,
ClientRegistration clientRegistration) {
// @formatter:off
return Mono.just(authorizeRequest)
.flatMap(this.contextAttributesMapper)
.map((attrs) -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
.principal(authorizeRequest.getPrincipal())
.attributes((attributes) -> {
if (!CollectionUtils.isEmpty(attrs)) {
attributes.putAll(attrs);
}
})
.build()
);
// @formatter:on
}
/**
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or
* re-authorizing) an OAuth 2.0 Client.
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider}
* used for authorizing (or re-authorizing) an OAuth 2.0 Client
*/
public void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
this.authorizedClientProvider = authorizedClientProvider;
}
/**
* Sets the {@code Function} used for mapping attribute(s) from the
* {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes to be associated to
* the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
* @param contextAttributesMapper the {@code Function} used for supplying the
* {@code Map} of attributes to the {@link OAuth2AuthorizationContext#getAttributes()
* authorization context}
*/
public void setContextAttributesMapper(
Function>> contextAttributesMapper) {
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
this.contextAttributesMapper = contextAttributesMapper;
}
/**
* Sets the handler that handles successful authorizations.
*
* The default saves {@link OAuth2AuthorizedClient}s in the
* {@link ServerOAuth2AuthorizedClientRepository}.
* @param authorizationSuccessHandler the handler that handles successful
* authorizations.
* @since 5.3
*/
public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
Assert.notNull(authorizationSuccessHandler, "authorizationSuccessHandler cannot be null");
this.authorizationSuccessHandler = authorizationSuccessHandler;
}
/**
* Sets the handler that handles authorization failures.
*
*
* A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} is used
* by default.
*
* @param authorizationFailureHandler the handler that handles authorization failures.
* @since 5.3
* @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
*/
public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
this.authorizationFailureHandler = authorizationFailureHandler;
}
/**
* The default implementation of the {@link #setContextAttributesMapper(Function)
* contextAttributesMapper}.
*/
public static class DefaultContextAttributesMapper
implements Function>> {
@Override
public Mono