Hi, since microsoft azure refresh token expires, I think would be nice to add this implementation to the class RefreshTokenOAuth2AuthorizedClientProvider to avoid 401 Unauthorized error.
Reading the documentation from microsoft azure at the link https://docs.microsoft.com/en-us/azure/active-directory-b2c/add-ropc-policy?tabs=app-reg-ga&pivots=b2c-user-flow
when you redeem a refresh token, obtain the following JWT:
{
"access_token": "eyJ0eXA...",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbG...",
"token_type": "Bearer",
"not_before": 1533672990,
"expires_in": 3600,
"expires_on": 1533676590,
"resource": "bef222...",
"id_token_expires_in": 3600,
"profile_info": "eyJ2ZXIiOiIxLjAiL...",
"refresh_token": "eyJraWQ...",
"refresh_token_expires_in": 1209600
}
the OAuth2AccessTokenResponse contains the additional property refresh_token_expires_in but the OAuth2RefreshToken.expiresAt is null. At the next redeem I can get a 401 Unauthorized error if the refresh token is expired.
I propose this change:
if present, set the refresh_token_expires_in to OAuth2RefreshToken.expiresAt then in RefreshTokenOAuth2AuthorizedClientProvider check the refresh token expire:
hasTokenExpired(authorizedClient.getRefreshToken())
if the refresh token is expired, then create a OAuth2PasswordGrantRequest instead a OAuth2RefreshTokenGrantRequest.
In this way you avoid the 401 Unauthorized error.
Thanks
Comment From: jzheaux
if the refresh token is expired, then create a OAuth2PasswordGrantRequest instead a OAuth2RefreshTokenGrantRequest.
I think this is likely too opinionated to bake into RefreshTokenOAuth2AuthorizedClientProvider. Once the refresh token is expired, reauthorization is necessary, but how that is done depends on the application. For example, REST APIs tend to return a 401 while web applications tend to redirect to a login page.
I'm curious, how are you obtaining the username and password to make something like what you are proposing work?
if present, set the refresh_token_expires_in to OAuth2RefreshToken.expiresAt
We don't tend to add vendor-specific support into the framework. Instead, what you can do is configure the RestOperations in the DefaultRefreshTokenResponseClient to massage the response in the manner you indicated.
Comment From: parmag
Hi, the use case is exactly the REST APIs. As far as your question is concerned, you can get the username and password from the OAuth2AuthorizationContext like the PasswordOAuth2AuthorizedClientProvider does. For example in the RefreshTokenOAuth2AuthorizedClientProvider you can do something like this:
if (hasTokenExpired(authorizedClient.getRefreshToken())) {
ClientRegistration clientRegistration = context.getClientRegistration();
...
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return null;
}
...
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, username,
password);
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, passwordGrantRequest);
}
In my humble opinion I don't think is a vendor specific support. Thanks in advance for your attention.
Comment From: jgrandja
I agree with @jzheaux, refresh_token_expires_in is a non-standard parameter and therefore should be implemented in the custom application.
Comment From: jzheaux
Thanks for your response, @parmag.
In my humble opinion I don't think is a vendor specific support.
My comment about being vendor-specific is related to the refresh_token_expires_in. Is there a specification where this is defined? As far as I know, this property is specific to your vendor (Microsoft).
As far as your question is concerned, you can get the username and password from the OAuth2AuthorizationContext like the PasswordOAuth2AuthorizedClientProvider does.
This doesn't make sense from a specification standpoint. The purpose of RefreshTokenOAuth2AuthorizedClientProvider is to support refresh tokens, which has nothing to do with the password grant. This kind of composition should be customized by the application.
More broadly, I don't think this pattern of renewing refresh tokens by using an in-memory username/password fallback option is a common enough setup to be included in the framework. When using a centralized authorization server, the REST API doesn't have the creds ever. When not using a centralized authorization server, the u/p is typically for a service-to-service call, meaning that the upstream service has the creds and can easily respond to the 401 by presenting them again.
Sometimes browser-based and native applications ask directly for the u/p and issue their own grant. In that case, holding onto the user's u/p in memory is a little scary. So, instead of caching the u/p, applications tend to do Refresh Token Rotation.
Comment From: parmag
Thanks @jzheaux and @jgrandja for your responses. I can understand your point of view, but for me using a refresh token that already I know is expired sounds not good. Make the re-authorization inside the RefreshTokenOAuth2AuthorizedClientProvider was only a proposal. I already solved my 401 problem with a try/catch pattern but I'm going to create a new client provider named for example RefreshTokenWithReAuthPasswordOAuth2AuthorizedClientProvider to manage the re-auth via password as I explained in my previous comment. To have a similar client available in the framework would have been nice... but ok... no problem.
Kind regards.
Comment From: jzheaux
but for me using a refresh token that already I know is expired sounds not good
Agreed, a request that uses an expired token should be denied. For a REST API, that usually means a 401.