Introduce Reactive OAuth2Authorization success/failure handlers
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
This commit is contained in:
@@ -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.
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Base exception for OAuth 2.0 Authorization errors.
|
||||
*
|
||||
@@ -30,7 +32,19 @@ public class OAuth2AuthorizationException extends RuntimeException {
|
||||
* @param error the {@link OAuth2Error OAuth 2.0 Error}
|
||||
*/
|
||||
public OAuth2AuthorizationException(OAuth2Error error) {
|
||||
super(error.toString());
|
||||
this(error, error.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationException} using the provided parameters.
|
||||
*
|
||||
* @param error the {@link OAuth2Error OAuth 2.0 Error}
|
||||
* @param message the exception message
|
||||
* @since 5.3
|
||||
*/
|
||||
public OAuth2AuthorizationException(OAuth2Error error, String message) {
|
||||
super(message);
|
||||
Assert.notNull(error, "error must not be null");
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
@@ -41,7 +55,20 @@ public class OAuth2AuthorizationException extends RuntimeException {
|
||||
* @param cause the root cause
|
||||
*/
|
||||
public OAuth2AuthorizationException(OAuth2Error error, Throwable cause) {
|
||||
super(error.toString(), cause);
|
||||
this(error, error.toString(), cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationException} using the provided parameters.
|
||||
*
|
||||
* @param error the {@link OAuth2Error OAuth 2.0 Error}
|
||||
* @param message the exception message
|
||||
* @param cause the root cause
|
||||
* @since 5.3
|
||||
*/
|
||||
public OAuth2AuthorizationException(OAuth2Error error, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
Assert.notNull(error, "error must not be null");
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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.
|
||||
@@ -53,6 +53,27 @@ public interface OAuth2ErrorCodes {
|
||||
*/
|
||||
String INVALID_SCOPE = "invalid_scope";
|
||||
|
||||
/**
|
||||
* {@code insufficient_scope} - The request requires higher privileges than
|
||||
* provided by the access token.
|
||||
* The resource server SHOULD respond with the HTTP 403 (Forbidden)
|
||||
* status code and MAY include the "scope" attribute with the scope
|
||||
* necessary to access the protected resource.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-3.1">RFC-6750 - Section 3.1 - Error Codes</a>
|
||||
*/
|
||||
String INSUFFICIENT_SCOPE = "insufficient_scope";
|
||||
|
||||
/**
|
||||
* {@code invalid_token} - The access token provided is expired, revoked,
|
||||
* malformed, or invalid for other reasons.
|
||||
* The resource SHOULD respond with the HTTP 401 (Unauthorized) status code.
|
||||
* The client MAY request a new access token and retry the protected resource request.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6750#section-3.1">RFC-6750 - Section 3.1 - Error Codes</a>
|
||||
*/
|
||||
String INVALID_TOKEN = "invalid_token";
|
||||
|
||||
/**
|
||||
* {@code server_error} - The authorization server encountered an
|
||||
* unexpected condition that prevented it from fulfilling the request.
|
||||
|
||||
@@ -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.
|
||||
@@ -58,6 +58,10 @@ class OAuth2AccessTokenResponseBodyExtractor
|
||||
ParameterizedTypeReference<Map<String, Object>> type = new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
BodyExtractor<Mono<Map<String, Object>>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type);
|
||||
return delegate.extract(inputMessage, context)
|
||||
.onErrorMap(e -> new OAuth2AuthorizationException(
|
||||
invalidTokenResponse("An error occurred parsing the Access Token response: " + e.getMessage()), e))
|
||||
.switchIfEmpty(Mono.error(() -> new OAuth2AuthorizationException(
|
||||
invalidTokenResponse("Empty OAuth 2.0 Access Token Response"))))
|
||||
.map(OAuth2AccessTokenResponseBodyExtractor::parse)
|
||||
.flatMap(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse)
|
||||
.map(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse);
|
||||
@@ -68,12 +72,19 @@ class OAuth2AccessTokenResponseBodyExtractor
|
||||
return TokenResponse.parse(new JSONObject(json));
|
||||
}
|
||||
catch (ParseException pe) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
|
||||
"An error occurred parsing the Access Token response: " + pe.getMessage(), null);
|
||||
OAuth2Error oauth2Error = invalidTokenResponse(
|
||||
"An error occurred parsing the Access Token response: " + pe.getMessage());
|
||||
throw new OAuth2AuthorizationException(oauth2Error, pe);
|
||||
}
|
||||
}
|
||||
|
||||
private static OAuth2Error invalidTokenResponse(String message) {
|
||||
return new OAuth2Error(
|
||||
INVALID_TOKEN_RESPONSE_ERROR_CODE,
|
||||
message,
|
||||
null);
|
||||
}
|
||||
|
||||
private static Mono<AccessTokenResponse> oauth2AccessTokenResponse(TokenResponse tokenResponse) {
|
||||
if (tokenResponse.indicatesSuccess()) {
|
||||
return Mono.just(tokenResponse)
|
||||
|
||||
@@ -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.
|
||||
@@ -30,6 +30,7 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -92,8 +93,23 @@ public class OAuth2BodyExtractorsTests {
|
||||
|
||||
Mono<OAuth2AccessTokenResponse> result = extractor.extract(response, this.context);
|
||||
|
||||
assertThatCode(() -> result.block())
|
||||
.isInstanceOf(RuntimeException.class);
|
||||
assertThatCode(result::block)
|
||||
.isInstanceOf(OAuth2AuthorizationException.class)
|
||||
.hasMessageContaining("An error occurred parsing the Access Token response");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oauth2AccessTokenResponseWhenEmptyThenException() {
|
||||
BodyExtractor<Mono<OAuth2AccessTokenResponse>, ReactiveHttpInputMessage> extractor = OAuth2BodyExtractors
|
||||
.oauth2AccessTokenResponse();
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
|
||||
|
||||
Mono<OAuth2AccessTokenResponse> result = extractor.extract(response, this.context);
|
||||
|
||||
assertThatCode(result::block)
|
||||
.isInstanceOf(OAuth2AuthorizationException.class)
|
||||
.hasMessageContaining("Empty OAuth 2.0 Access Token Response");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user