Skip to content

Introduce Reactive OAuth2Authorization success/failure handlers #7756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,7 +17,10 @@

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

import java.util.Collections;
Expand All @@ -26,10 +29,33 @@

/**
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
* that is capable of operating outside of a {@code ServerHttpRequest} context,
* 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 #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.</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
Expand All @@ -45,6 +71,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 +87,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,12 +120,33 @@ private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2Author
}));
}

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

Expand All @@ -121,6 +172,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 All @@ -134,4 +215,5 @@ public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest)
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
}
}

}
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 Authorized 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 Authorized 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