-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
Introduce Reactive OAuth2Authorization success/failure handlers #7699
Comments
@philsttr This sounds like a valuable enhancement. Would you be interested in submitting a PR for this? |
Yeah, I could tackle the implementation, once we settle on an approach. I'd like your input on what approach to take, before I get started. My current thinking is to:
We could also combine this approach somewhat with the approach I mentioned in the issue description, where Your thoughts? (It would also be great if #7702 was merged before I start on this, so I can modify |
@philsttr I'm not sure if I'm leaning towards introducing 2 new strategies similar to For example: public interface ServerOAuth2AuthorizationSuccessHandler {
Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, ...);
}
public interface ServerOAuth2AuthorizationFailureHandler {
Mono<Void> onAuthorizationFailure(OAuth2AuthorizationException exception, ...);
} We could consider extending The default implementation of Thoughts? |
Related #7583 |
A few questions/comments: What would have a reference to the handlers, detect authentication success/failure, and call the handlers? Since This is where I was thinking that
Also, is "authorization" is the proper term here? I was thinking "authentication" is more correct. When the token is no longer valid, that is an authentication failure and a 401 returned. If the token was valid, but did not have the proper scopes, that would be an authorization failure, and a 403 would be returned. The case being handled here is the former. |
The
We also would need to handle 401 errors in
Yes, there would need to be two since they call different collaborators. Another advantage of having these handlers is that user's can customize the behaviour (via setters) above the standard behaviour of simply removing the authorized client from the repository/service.
Yes, authentication is more correct when a 401 occurs during a protected resource request because the token is no longer valid (expired/revoked). However, what about the cases when the client attempts to obtain a new token or renew/refresh an existing expired token? For these cases the authorization fails for the client and typically a 400 is returned as per spec. I'm going with the term authorization to suit all cases. Makes sense? |
Ok, I think I'm following you now. I was only thinking about using these handlers when communicating with a resource server. But you make an excellent point that these same handlers could be used when communicating with the authorization server to retrieve/refresh a token (your cases 1-3)
Yeah, this was the original use case for this issue. I'd like to have
Without doing the above, I don't see how I can make
Fair point. I'm on board with using "authorization" since we're also now including use cases for handling responses from the authorization server. I was originally only looking at the use case for handling responses from a resource server. |
I'd rather keep the
The default If the application uses the constructor |
Ok, that works for me. I think I have enough to start on an implementation. I have a couple other things on my plate, but should be able to start soon. |
Sounds good and thanks for all your great work @philsttr |
Next observations after looking deeper into this: 1. It seems like this change also needs to be made in non-reactive environments. I'll take a look at this as well as part of this issue. 2. I'm trying to figure out where to get the values to pass as the arguments needed by the *OAuth2AuthorizedClientRepository/Service save/removeAuthorizedClient methods. Specifically:
For public interface ServerOAuth2AuthorizationSuccessHandler {
Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal);
}
public interface ServerOAuth2AuthorizationFailureHandler {
Mono<Void> onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal);
}
public interface OAuth2AuthorizationSuccessHandler {
void onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal);
}
public interface OAuth2AuthorizationFailureHandler {
void onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal);
} For So, now I'm back to just including Do you have any recommendations here? 3. I think that the 4. Regarding the use of
If we use 5. Where to create and handle the exceptions Given the changes in 2. above, the only additional thing the exception would need is the I'm trying to figure out where the exception would actually be created. My current thoughts are:
Does that make sense? In any case, thank you for your insights and patience as I look through this. I'm just trying to nail down the design before I bang my head against the wall on implementation. |
(1)
Yes it does. I was planning on adding another ticket for the Servlet implementation. However, it would be ideal to have both merged together. Thanks for looking into this as well ! (2)
Agreed. We should pass in the
I feel we need to pass in all required arguments into the handler. We just need to figure out the best way to do this from an API viewpoint.
The handler doesn't need access to the authorize request. The
Yeah, we definitely don't want to create 2 sets of handlers. If we look at the design so far for reactive we have this: package org.springframework.security.oauth2.client.web.server;
public interface ServerOAuth2AuthorizationSuccessHandler {
Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient,
Authentication principal,
@Nullable ServerWebExchange exchange);
}
public interface ServerOAuth2AuthorizationFailureHandler {
Mono<Void> onAuthorizationFailure(OAuth2AuthorizationException exception,
Authentication principal,
@Nullable ServerWebExchange exchange);
} I'm not sure if I'm keen on the I'm wondering if we could borrow the pattern used for Let's say we have this design instead: package org.springframework.security.oauth2.client;
public interface ReactiveOAuth2AuthorizationSuccessHandler {
Mono<Void> onAuthorizationSuccess(OAuth2AuthorizationSuccessEvent successEvent);
}
public interface ReactiveOAuth2AuthorizationFailureHandler {
Mono<Void> onAuthorizationFailure(OAuth2AuthorizationFailureEvent failureEvent);
}
public final class OAuth2AuthorizationSuccessEvent {
private OAuth2AuthorizedClient authorizedClient;
private Authentication principal;
private Map<String, Object> attributes;
...
}
public final class OAuth2AuthorizationFailureEvent {
private OAuth2AuthorizationException exception;
private Authentication principal;
private Map<String, Object> attributes;
...
} NOTE: I'm not convinced on the suffix naming The This pattern worked well for What are your thoughts on this approach? (3)
Yes I agree. However, it might be strange not to have symmetry here. For now, I would configure the failure handler only but I'm not sure we need to expose a setter during first iteration. Let's keep it internal for now and see how it works out. (4)
Take a look at "Bearer Token Usage: Error Codes":
So the resource server may respond with other status codes than 401. The way I'm looking at this is we're dealing with OAuth 2.0 protected resources which is all about authorization. OpenID Connect is all about authentication. I agree that a 401 returned from a resource server because of a invalid (expired) token may look strange if we're handling FYI, the base exception Does this clear things up or do you still have some concerns? (5)
Yes, this is the way I see it being handled as well. FYI, most (if not all)
Yes but you'll have to look for more than just 401, as indicated by "Bearer Token Usage: Error Codes". Great questions and sorry for the long response. But I think we're really close on design now. |
(1) 👍 (2) I like the introduction of *Event with attributes. Excellent idea. (3) 👍 (4) Ok, yeah, after looking at the rfc, I can see now how an (5) 👍 (6) I can see two options for For A, I see there is already a Or maybe introduce a new class between Or introduce one or more new subclasses of For B, it would be fairly simple, since all places invoking the handler have access to the I don't have a strong opinion on this, so I'd like to defer to you as a maintainer of the library. |
(6)
We could than push How does that sound? Just a heads up that I'll be off for the holidays between Dec 23 - Jan 3 (inclusive). And next week I'm pretty swamped as well. I will be available next week but just wanted to let you know so you are not rushing to implement this feature :) Thanks again for taking this on. It will be a valuable add to client. |
I made a first pass at introducing reactive success/failure handlers today. See the https://github.com/philsttr/spring-security/tree/gh-7699 branch for work-in-progress. I have lots of unit tests to write, but the basics are there. |
That is an excellent first pass @philsttr ! I'm excited about this new feature ! Thank you for your great work 👍 |
@philsttr To make things easier during the review process, let's focus on the reactive implementation first and submit in it's own PR. After it's merged, we can switch to the Servlet implementation. |
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 spring-projectsgh-7699
Summary
When an OAuth2 Client sends a token to a downstream OAuth2 Resource Server, and the OAuth2 Resource Server returns an HTTP 401 response, I would like to prevent the OAuth2 Client from sending that same token in future requests (and instead retrieve a new token for future requests).
Currently, spring-security-oauth2-client provides good support for re-retrieving/refreshing tokens before they expire. However, spring-security-oauth2-client does not provide good support for re-retrieving/refreshing access tokens when they have become invalid for reasons other than expiration. A good signal that a token is no longer valid is when an OAuth2 Resource Server returns an HTTP 401 response to the client.
Both
ServerOAuth2AuthorizedClientRepository
andReactiveOAuth2AuthorizedClientService
provide aremoveAuthorizedClient
method to remove the cachedOAuth2AuthorizedClient
. It would be nice if spring-security-oauth2-client provided something that actually called that method when an HTTP 401 response is received from an OAuth2 Resource Server.A possible implementation might be:
DefaultReactiveOAuth2AuthorizedClientManager
(and the futureAuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
from Reactive Implementation of AuthorizedClientServiceOAuth2AuthorizedCli… #7589) could put aMono<Void>
in the subscriber context that will remove the token from the cache when subscribedDefaultReactiveOAuth2AuthorizedClientManager
would put aMono<Void>
that invokes itsServerOAuth2AuthorizedClientRepository.removeAuthorizedClient
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
would put aMono<Void>
that invokes itsReactiveOAuth2AuthorizedClientService.removeAuthorizedClient
ExchangeFilterFunction
(or one of the existing ones) checks for HTTP 401 responses, and chains theMono<Void>
from step 1 if it existsExchangeFilterFunction
from the implementation of how to remove the cached authorized client (i.e. separate filter function implementations would not be needed depending on whether aServerOAuth2AuthorizedClientRepository
orReactiveOAuth2AuthorizedClientService
is being used to persistOAuth2AuthorizedClient
s)Mono<Void>
)(After looking at this a bit more, I realize the above approach probably won't work, since anything put into the subscriber context by a
ReactiveOAuth2AuthorizedClientManager
wouldn't actually be visible downstream in anExchangeFilterFunction
. So, something else is needed. Perhaps something inServerOAuth2AuthorizedClientExchangeFilterFunction
)Actual Behavior
spring-security-oauth2-client continues to reuse an OAuth2 token, even after an OAuth2 Resource server has returned a 401 response, which usually indicates the token is invalid.
Expected Behavior
spring-security-oauth2-client removes the cached token after an OAuth2 Resource server has returned a 401 response. Future requests will trigger a re-retrieval of a new token.
Configuration
DefaultReactiveOAuth2AuthorizedClientManager
orAuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
from Reactive Implementation of AuthorizedClientServiceOAuth2AuthorizedCli… #7589Version
5.2.1.RELEASE
The text was updated successfully, but these errors were encountered: