Skip to content

Commit

Permalink
Introduce Reactive OAuth2Authorization success/failure handlers
Browse files Browse the repository at this point in the history
All ReactiveOAuth2AuthorizedClientManagers now have authorization success/failure handlers.
A success handler is provided to save authorized clients for future requests.
A failure handler is provided to remove previously saved authorized clients.

ServerOAuth2AuthorizedClientExchangeFilterFunction also makes use of a
failure handler in the case of unauthorized or forbidden http status code.

The main use cases now handled are
- remove authorized client when an authorization server indicates that a refresh token is no longer valid (when authorization server returns invalid_grant)
- remove authorized client when a resource server indicates that an access token is no longer valid (when resource server returns invalid_token)

Introduced ClientAuthorizationException to capture details needed when removing an authorized client.
All ReactiveOAuth2AccessTokenResponseClients now throw a ClientAuthorizationException on failures.

Created AbstractWebClientReactiveOAuth2AccessTokenResponseClient to unify common logic between all ReactiveOAuth2AccessTokenResponseClients.

Fixes gh-7699
  • Loading branch information
philsttr authored and jgrandja committed Jan 16, 2020
1 parent 7f9715d commit e5fca61
Show file tree
Hide file tree
Showing 26 changed files with 2,504 additions and 480 deletions.
Original file line number Diff line number Diff line change
@@ -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");
* you may not use this file except in compliance with the License.
Expand All @@ -17,25 +17,55 @@

import org.springframework.security.core.Authentication;
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.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.Map;
import java.util.function.Function;

/**
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
* that is capable of operating outside of a {@code ServerHttpRequest} context,
* An implementation of a {@link ReactiveOAuth2AuthorizedClientManager}
* that is capable of operating outside of the context of a {@link ServerWebExchange},
* e.g. in a scheduled/background thread and/or in the service-tier.
*
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
* <p>(When operating <em>within</em> the context of a {@link ServerWebExchange},
* use {@link DefaultReactiveOAuth2AuthorizedClientManager} instead.)</p>
*
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}.</p>
*
* <h2>Authorized Client Persistence</h2>
*
* <p>This client manager utilizes a {@link ReactiveOAuth2AuthorizedClientService}
* to persist {@link OAuth2AuthorizedClient}s.</p>
*
* <p>By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
* will be saved in the authorized client service.
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler}
* via {@link #setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)}.</p>
*
* <p>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 service.
* (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)}.</p>
*
* @author Ankur Pathak
* @author Phil Clay
* @see ReactiveOAuth2AuthorizedClientManager
* @see ReactiveOAuth2AuthorizedClientProvider
* @see ReactiveOAuth2AuthorizedClientService
* @see ReactiveOAuth2AuthorizationSuccessHandler
* @see ReactiveOAuth2AuthorizationFailureHandler
* @since 5.2.2
*/
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
Expand All @@ -45,6 +75,8 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;

/**
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
Expand All @@ -59,14 +91,16 @@ public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
this.authorizationSuccessHandler = new SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler(authorizedClientService);
this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(authorizedClientService);
}

@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");

return createAuthorizationContext(authorizeRequest)
.flatMap(this::authorizeAndSave);
.flatMap(authorizationContext -> authorize(authorizationContext, authorizeRequest.getPrincipal()));
}

private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
Expand All @@ -90,13 +124,34 @@ private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2Author
}));
}

private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
/**
* 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)
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
// Delegate to the authorizationSuccessHandler of the successful authorization
.flatMap(authorizedClient -> this.authorizationSuccessHandler.onAuthorizationSuccess(
authorizedClient,
authorizationContext.getPrincipal())
principal,
Collections.emptyMap())
.thenReturn(authorizedClient))
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
// Delegate to the authorizationFailureHandler of the failed authorization
.onErrorResume(OAuth2AuthorizationException.class, authorizationException -> this.authorizationFailureHandler.onAuthorizationFailure(
authorizationException,
principal,
Collections.emptyMap())
.then(Mono.error(authorizationException)))
.switchIfEmpty(Mono.defer(() -> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
}

/**
Expand All @@ -121,6 +176,36 @@ public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map
this.contextAttributesMapper = contextAttributesMapper;
}

/**
* Sets the handler that handles successful authorizations.
*
* <p>A {@link SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler}
* is used by default.</p>
*
* @param authorizationSuccessHandler the handler that handles successful authorizations.
* @see SaveAuthorizedClientReactiveOAuth2AuthorizationSuccessHandler
* @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.
*
* <p>A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
* is used by default.</p>
*
* @param authorizationFailureHandler the handler that handles authorization failures.
* @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
* @since 5.3
*/
public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
this.authorizationFailureHandler = authorizationFailureHandler;
}

/**
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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;

import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert;

/**
* This exception is thrown on the client side when an attempt to authenticate
* or authorize an OAuth 2.0 client fails.
*
* @author Phil Clay
* @since 5.3
* @see OAuth2AuthorizedClient
*/
public class ClientAuthorizationException extends OAuth2AuthorizationException {

private final String clientRegistrationId;

/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
*/
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId) {
this(error, clientRegistrationId, error.toString());
}
/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
* @param message the exception message
*/
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, String message) {
super(error, message);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
this.clientRegistrationId = clientRegistrationId;
}

/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
* @param cause the root cause
*/
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, Throwable cause) {
this(error, clientRegistrationId, error.toString(), cause);
}

/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
* @param message the exception message
* @param cause the root cause
*/
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, String message, Throwable cause) {
super(error, message, cause);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
this.clientRegistrationId = clientRegistrationId;
}

/**
* Returns the identifier for the client's registration.
*
* @return the identifier for the client's registration
*/
public String getClientRegistrationId() {
return this.clientRegistrationId;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
Expand All @@ -15,9 +15,7 @@
*/
package org.springframework.security.oauth2.client;

import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert;

/**
* This exception is thrown when an OAuth 2.0 Client is required
Expand All @@ -27,9 +25,8 @@
* @since 5.1
* @see OAuth2AuthorizedClient
*/
public class ClientAuthorizationRequiredException extends OAuth2AuthorizationException {
public class ClientAuthorizationRequiredException extends ClientAuthorizationException {
private static final String CLIENT_AUTHORIZATION_REQUIRED_ERROR_CODE = "client_authorization_required";
private final String clientRegistrationId;

/**
* Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters.
Expand All @@ -38,17 +35,7 @@ public class ClientAuthorizationRequiredException extends OAuth2AuthorizationExc
*/
public ClientAuthorizationRequiredException(String clientRegistrationId) {
super(new OAuth2Error(CLIENT_AUTHORIZATION_REQUIRED_ERROR_CODE,
"Authorization required for Client Registration Id: " + clientRegistrationId, null));
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
this.clientRegistrationId = clientRegistrationId;
}

/**
* Returns the identifier for the client's registration.
*
* @return the identifier for the client's registration
*/
public String getClientRegistrationId() {
return this.clientRegistrationId;
"Authorization required for Client Registration Id: " + clientRegistrationId, null),
clientRegistrationId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import reactor.core.publisher.Mono;

import java.util.Map;

/**
* Handles when an OAuth 2.0 Client
* fails to authorize (or re-authorize)
* via the authorization server or resource server.
*
* @author Phil Clay
* @since 5.3
*/
@FunctionalInterface
public interface ReactiveOAuth2AuthorizationFailureHandler {

/**
* Called when an OAuth 2.0 Client
* fails to authorize (or re-authorize)
* via the authorization server or resource server.
*
* @param authorizationException the exception that contains details about what failed
* @param principal the {@code Principal} that was attempted to be authorized
* @param attributes an immutable {@code Map} of extra optional attributes present under certain conditions.
* For example, this might contain a {@link org.springframework.web.server.ServerWebExchange ServerWebExchange}
* if the authorization was performed within the context of a {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished handling the event.
*/
Mono<Void> onAuthorizationFailure(
OAuth2AuthorizationException authorizationException,
Authentication principal,
Map<String, Object> attributes);
}
Loading

0 comments on commit e5fca61

Please sign in to comment.