Skip to content

Allow for a configurable strategy for granting refresh_token #1430

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
jgrandja opened this issue Oct 30, 2023 · 8 comments
Closed

Allow for a configurable strategy for granting refresh_token #1430

jgrandja opened this issue Oct 30, 2023 · 8 comments
Assignees
Labels
type: enhancement A general enhancement
Milestone

Comments

@jgrandja
Copy link
Collaborator

We should consider allowing for a configurable strategy for granting refresh_token.

The solution should take into consideration for allowing refresh_token to be granted to public clients, but would be the responsibility of the consuming application to configure the strategy.

Related gh-297

@jgrandja jgrandja added the type: enhancement A general enhancement label Oct 30, 2023
@jgrandja jgrandja self-assigned this Oct 30, 2023
@jgrandja jgrandja added this to the 1.3.x milestone Oct 30, 2023
jgrandja added a commit to jgrandja/spring-authorization-server that referenced this issue Nov 2, 2023
@jgrandja jgrandja modified the milestones: 1.3.x, 1.2.0 Nov 2, 2023
@jgrandja jgrandja changed the title Consider allowing for a configurable strategy for granting refresh_token Allow for a configurable strategy for granting refresh_token Nov 7, 2023
@stefanocke
Copy link

stefanocke commented Dec 4, 2023

@jgrandja , I was able to get refresh tokens for a public client as shown in your test case.
However, I was not able to use them afterwards:
I call the token endpoint with the refresh token and wiht grant_type "refresh_token", but I get an InsufficientAuthenticationException ("Full authentication is required to access this resource").

During debugging I have seen so far, that OAuth2TokenEndpointFilter and OAuth2RefreshTokenAuthenticationConverter are not even called at all.
My guess is that the reason is the missing OAuth2ClientAuthenticationToken, since none of the AuthenticationConverters applies here, neither ClientSecretBasicAuthenticationConverter, nor ClientSecretPostAuthenticationConverter. And especially not PublicClientAuthenticationConverter , since it is only for PKCE. But this is not applicable when sending a refresh_token to the token endpoint, but only for authorization_code grant. Likewise, an according AuthenticationProvider is missing, since PublicClientAuthenticationProvider only works for PKCE again.
So, the AuthorizationFilter fails due to missing authentication, and OAuth2TokenEndpointFilter, which is afterwards, is never reached.
There are extension points for both, AuthenticationConverters and AuthenticationProviders. So, would it be the right way to write a converter that looks up the client by the refresh token and then a provider that authenticates the public client by that?
I guess that could work, but I am a bit afraid to open some new security holes here.

Update:
I haved solved it as pointed out above, by a custom AuthenticationConverter and AuthenticationProvider, which are configured like this:

authorizationServerConfigurer.clientAuthentication(ca -> {
            ca.authenticationConverters(converters -> 
                     converters.add(new PublicClientRefreshTokenAuthenticationConverter()));
            ca.authenticationProviders(providers -> PublicClientRefreshTokenSupportingAuthenticationProvider
                    .createAndInsert(providers, registeredClientRepository, authorizationService));
        });
  • The converter looks for "refresh_token" grant and checks the refresh token parameter. If both are present, an unauthenticated OAuth2ClientAuthenticationToken is created. I had to tweak a bit here: Since a client id is required, I provide some fake id not used by real clients.
  • The authentication provider looks for ClientAuthenticationMethod.NONE and for the fake id. Then it uses the OAuth2AuthorizationService to look up the OAuth2Authorization by the refresh token. Then, it gets the registered client from it. Finally, it creates an authenticated OAuth2ClientAuthenticationToken with the registered client.

@jgrandja
Copy link
Collaborator Author

jgrandja commented Dec 5, 2023

@stefanocke PublicClientRefreshTokenSupportingAuthenticationProvider is not needed as you can (and should) leverage the existing OAuth2RefreshTokenAuthenticationProvider. You only need to provide a custom AuthenticationConverter that ultimately returns an OAuth2ClientAuthenticationToken instance where OAuth2ClientAuthenticationToken.isAuthenticated() is true. You can see a similar implementation here.

@stefanocke
Copy link

stefanocke commented Dec 5, 2023

@jgrandja , thanks for your feedback. I am not sure I understand your instructions.

  • You say, I need an authenticated OAuth2ClientAuthenticationToken.
  • For this, I need the RegisteredClient.
  • In your example, the RegisteredClient is determined by a DeviceClientAuthenticationProvider
  • Very similar to this, I have the PublicClientRefreshTokenSupportingAuthenticationProvider, which also determines the RegisteredClient and creates an authenticated OAuth2ClientAuthenticationToken.
    • The only difference is, that I have no client id, but a refresh token, which was issued in the context of a certain client.
    • See also https://datatracker.ietf.org/doc/html/rfc6749#page-47 : There is no clientId parameter here in the request. So if there is no client auth, we can only determine the client by the refresh token.
  • The OAuth2RefreshTokenAuthenticationProvider is still used later, but not for client authentication, but for authenticating the resource owner (like in all other cases)
  • Of course I could try to squash my Provider and Converter into a Converter that immediately gives an authenticated OAuth2ClientAuthenticationToken. But is this really the right way considering that in all other cases and even in your example it is separated?

@jgrandja
Copy link
Collaborator Author

jgrandja commented Dec 5, 2023

@stefanocke

I have no client id, but a refresh token, which was issued in the context of a certain client.

See also https://datatracker.ietf.org/doc/html/rfc6749#page-47 : There is no clientId parameter here in the request. So if there is no client auth, we can only determine the client by the refresh token.

Even for public (unauthenticated) clients, the client_id parameter MUST be sent to identify the calling client. The client_id parameter will then be compared to client_id associated to the refresh_token, to prevent token replay.

In section 6, Refreshing an Access Token:

If the client type is confidential or
the client was issued client credentials (or assigned other
authentication requirements), the client MUST authenticate with the
authorization server as described in Section 3.2.1.

Although, the spec does not state anything about public clients, the token endpoint is a protected resource and therefore requires some form of client authentication. For public clients, the client_id is required at a minimum.

@stefanocke
Copy link

stefanocke commented Dec 5, 2023

@jgrandja , in Section 3.2.1. we have:

A client MAY use the "client_id" request parameter to identify itself
when sending requests to the token endpoint. In the
"authorization_code" "grant_type" request to the token endpoint, an
unauthenticated client MUST send its "client_id" to prevent itself
from inadvertently accepting a code intended for a client with a
different "client_id".

IMHO MAY != MUST, so I can't rely on the "client_id" request parameter. (It is only a MUST for "authorization_code" grant type, but not for "refresh_token")
Of course, if it is there, I should use it. But otherwise, I have only the refresh token to identify the client.

But isn't this quite independent from the question regarding separating the logic into a Converter and Provider? The Converter just does the HTTP request parameter extraction and is dumb otherwise. The Provider looks up the RegisteredClient, as in your example.

@TryAndErrorBot
Copy link

TryAndErrorBot commented Dec 21, 2023

Hi @jgrandja.

I have a similar scenario and wanted to create a new access token for a public client using a refresh token. Now I have taken the following example from you:

PublicClientRefreshTokenSupportingAuthenticationProvider is not needed as you can (and should) leverage the existing OAuth2RefreshTokenAuthenticationProvider. You only need to provide a custom AuthenticationConverter that ultimately returns an OAuth2ClientAuthenticationToken instance where OAuth2ClientAuthenticationToken.isAuthenticated() is true. You can see a similar implementation here.

I have integrated a self-implemented AuthenticationConverter and set the existing OAuth2RefreshTokenAuthenticationProvider as the provider, but the scenario does not work in this case.

The self-implemented converter returns an OAuth2ClientAuthenticationToken (as specified in your example), but the provider class OAuth2RefreshTokenAuthenticationProvider only supports the following type of token: OAuth2RefreshTokenAuthenticationToken. The problem is that the provider is not pulled in the ProviderManager due to the above-mentioned circumstance.

Here is the code from the OAuth2RefreshTokenAuthenticationProvider class (254-256).

@Override
public boolean supports(Class<?> authentication) {
   return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}

If I am making a mistake, I would be grateful for any help. Possibly the topic could have been discussed at Stackoverflow, but I thought that it might fit to this issue, since I take up the topic.

I have now followed your example and could also create my own provider to realise the functionality. I wanted to ask out of interest if I have made a mistake or if this way cannot work.

Thanks for the help or information.

@mberwanger
Copy link

mberwanger commented Jan 9, 2024

@jgrandja I am also experiencing the same problem and this is my read on the situation.

OAuth2TokenEndpointFilter is protected by the AuthorizationFilter which requires a valid Authentication object to continue on the filter chain. On the Authorization Code Flow with PKCE, the OAuth2ClientAuthenticationFilter uses the PublicClientAuthenticationConverter and PublicClientAuthenticationProvider to provide the Authentication. However when a public client tries to redeem a refresh token (by providing only the client id) there is nothing currently that provides that valid Authentication object.

I implemented a custom AuthenticationConverter but that throws a OAuth2AuthenticationException when the PublicClientAuthenticationProvider tries to validate the public client. That validator assumes its only going to be the authorization_code flow, which up to recently was the case.

Thanks again

@jgrandja
Copy link
Collaborator Author

@stefanocke @TryAndErrorBot @mberwanger

See this comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
4 participants