spring-authorization-server icon indicating copy to clipboard operation
spring-authorization-server copied to clipboard

Registered scopes should not be defaulted for client_credentials grant

Open jgrandja opened this issue 3 years ago • 7 comments

If scope is not requested by the client for the client_credentials grant flow, the client's registered scope(s) are used as the defaults for the authorized access token. This behaviour needs to be changed to only include the requested scope(s) only. Furthermore, if no scopes are requested then the access token should still be granted but will exclude the scope claim.

Defaulting scope(s) is the responsibility of the consuming application.

Related gh-600, gh-737, gh-738

jgrandja avatar Jun 13 '22 14:06 jgrandja

If a client does not request scopes the default consent page displays a confirmation page that allows you to submit consent to no scopes. After consent is provided OAuth2AuthorizationcodeRequestAuthenticationProvider fails because there were no scopes. Ideally, if no scopes requested by the client should cause it to fail the failure should happen as soon as no scopes were provided rather than after consenting.

rwinch avatar Jun 24 '22 18:06 rwinch

Hi all i'm in the process of evaluating the new authorization server project to replace our internal oauth2 which was based on the deprecated spring-security-oauth2. one of the issues i encounter is that, the default behaviour as is documented in DefaultOAuth2RequestFactory is not possible anymore

if ((scopes == null || scopes.isEmpty())) {
  // If no scopes are specified in the incoming data, use the default values registered with the client 
  // (the spec allows us to choose between this option and rejecting the request completely, so we'll take the 
  // least obnoxious choice as a default). 
      scopes clientDetails.getScope();
}

in short if no specific scope set was requested, the set was "defaulted" to the configured set of the client. note that none of our resource owners use consent because the data is not linked to user like phone/email but to tenants , so user consent is not used in our case and defaults to always allow all.

with the new project, using version 0.3.1, i see when requesting access tokens, without specifying scopes, a empty scope list is returned which breaks scope validation later on when using the tokens.

reading https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05, section 3.2.2.1,

i read it's up to the authorization server to determane the resulting scopes , defaulting to preset or error this. so the "old" behaviour seems still valid in this case. this both for authorization_code and client_credentials grants.

lucwillems avatar Jun 30 '22 12:06 lucwillems

just a quick thought , to satisfy all use case , we could delegate the finalizing of the scope set to customizable scope handler. this handler could

  • verify set of request scopes is valid for an given client
  • create a final authorized set of scopes depending on client configuration and "empty" parameter handling
  • use this final list in the token creation as authorized scopes

each AuthenticationConverter implementation should be able to retrieve a handler so that different request handle scopes on there required way. the defaultHandler could then provide empty set behaviour , and custom handler could return whatever he see feasible for that use case. based on client configuration we can controle the behaviour.

lucwillems avatar Jun 30 '22 14:06 lucwillems

@rwinch I believe that the behavior that you are describing is exactly what this issue is meant to resolve. Once this issue is completed, a request with no scope parameter will be valid and will result in a token/token response with no scope.

mikesaurus avatar Jun 30 '22 15:06 mikesaurus

@lucwillems Since Spring is defaulting to "no scope" when the client requests "no scope" couldn't you do what you are proposing using a custom AuthenticationConverter?

mikesaurus avatar Jun 30 '22 16:06 mikesaurus

i've been looking at that, but as far i can see now, it means customising all AuthenticationConverter and copy 90% of original code, and for TokenEndPointFilter we have A DelegatedConverter including 3 other converters . so there is also work needed.

doesn't seem optimal to me. also see the related gh-600 and my comments there.

lucwillems avatar Jun 30 '22 17:06 lucwillems

@lucwillems I'm not sure what you need to customize for your use cases, but if you only want to customize the scope then it shouldn't be as involved as that. There's definitely no need to copy the default converter code. You can follow the advice in https://github.com/spring-projects/spring-authorization-server/issues/600 and delegate most of the work to the default converter. Then, you only need to add your own code to customize the scope on the resulting authentication object.

Just threw this together as an example:

public class CustomAuthorizationCodeRequestConverter implements AuthenticationConverter {
    private final OAuth2AuthorizationCodeRequestAuthenticationConverter authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
    private final RegisteredClientRepository clientRepository;

    public CustomAuthorizationCodeRequestConverter(final RegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
    }

    @Override
    public Authentication convert(final HttpServletRequest request) {
        if (POST.matches(request.getMethod())) {
            return authenticationConverter.convert(request);
        }

        return convertAuthorizationRequest(request);
    }

    private Authentication convertAuthorizationRequest(final HttpServletRequest request) {
        final OAuth2AuthorizationCodeRequestAuthenticationToken authenticationToken = (OAuth2AuthorizationCodeRequestAuthenticationToken) authenticationConverter.convert(request);

        if (isEmpty(authenticationToken.getScopes())) {
            final String clientId = authenticationToken.getClientId();

            return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, (Authentication) authenticationToken.getPrincipal())
                    .authorizationUri(authenticationToken.getAuthorizationUri())
                    .redirectUri(authenticationToken.getRedirectUri())
                    .scopes(clientRepository.findByClientId(clientId).getScopes())
                    .state(authenticationToken.getState())
                    .additionalParameters(authenticationToken.getAdditionalParameters())
                    .consent(authenticationToken.isConsent())
                    .build();
        }

        return authenticationToken;
    }
}

That would be used for the authorize endpoint. For the token endpoint, if we're still just talking about scope, I think that you only need to customize the OAuth2ClientCredentialsAuthenticationConverter in a similar manner. Then, you'd just use your own DelegatingAuthenticationConverter instance containing your customer client credentials converter along with the standard OAuth2RefreshTokenAuthenticationConverter and OAuth2AuthorizationCodeAuthenticationConverter converters.

Sorry, if I'm missing some of what you're trying to do, but hopefully this helps to simplify your implementation a bit.

mikesaurus avatar Jun 30 '22 19:06 mikesaurus

@rwinch Regarding your comment:

If a client does not request scopes the default consent page displays a confirmation page that allows you to submit consent to no scopes...

The use case where the client does not request scope but is configured for authorization consent is invalid. It doesn't make sense to require authorization consent for the client when it doesn't request any scopes for the resource owner to consent to. This use case happens when the RegisteredClient is configured with ClientSettings.requireAuthorizationConsent(true). If you update the configuration to ClientSettings.requireAuthorizationConsent(false) and request without scope then the access token will be generated without any scope claim associated to it.

Furthermore, the default consent page is designed to work with scope-based authorization only. This is why the access_denied error occurs because no scope is available to authorize so when the authorization consent is processed the assumption is that an empty authority (scope) list means the authorization request is not authorized (approved).

Also, the default consent page is meant to be used for development only. In a production application a custom consent page would be configured and has the flexibility to handle any type of authorization consent without being limited to scope-based authorization consent only. For example, the consent page may display claims, permissions, roles, etc. The following test demonstrates the custom configuration for this use case.

https://github.com/spring-projects/spring-authorization-server/blob/2b47a1695659b9df7e050a1aa8e3cbd3936c8e72/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java#L564

jgrandja avatar Aug 16 '22 10:08 jgrandja

@lucwillems I'm not sure what you need to customize for your use cases, but if you only want to customize the scope then it shouldn't be as involved as that. There's definitely no need to copy the default converter code. You can follow the advice in #600 and delegate most of the work to the default converter. Then, you only need to add your own code to customize the scope on the resulting authentication object.

Just threw this together as an example:

public class CustomAuthorizationCodeRequestConverter implements AuthenticationConverter {
    private final OAuth2AuthorizationCodeRequestAuthenticationConverter authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
    private final RegisteredClientRepository clientRepository;

    public CustomAuthorizationCodeRequestConverter(final RegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
    }

    @Override
    public Authentication convert(final HttpServletRequest request) {
        if (POST.matches(request.getMethod())) {
            return authenticationConverter.convert(request);
        }

        return convertAuthorizationRequest(request);
    }

    private Authentication convertAuthorizationRequest(final HttpServletRequest request) {
        final OAuth2AuthorizationCodeRequestAuthenticationToken authenticationToken = (OAuth2AuthorizationCodeRequestAuthenticationToken) authenticationConverter.convert(request);

        if (isEmpty(authenticationToken.getScopes())) {
            final String clientId = authenticationToken.getClientId();

            return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, (Authentication) authenticationToken.getPrincipal())
                    .authorizationUri(authenticationToken.getAuthorizationUri())
                    .redirectUri(authenticationToken.getRedirectUri())
                    .scopes(clientRepository.findByClientId(clientId).getScopes())
                    .state(authenticationToken.getState())
                    .additionalParameters(authenticationToken.getAdditionalParameters())
                    .consent(authenticationToken.isConsent())
                    .build();
        }

        return authenticationToken;
    }
}

That would be used for the authorize endpoint. For the token endpoint, if we're still just talking about scope, I think that you only need to customize the OAuth2ClientCredentialsAuthenticationConverter in a similar manner. Then, you'd just use your own DelegatingAuthenticationConverter instance containing your customer client credentials converter along with the standard OAuth2RefreshTokenAuthenticationConverter and OAuth2AuthorizationCodeAuthenticationConverter converters.

Sorry, if I'm missing some of what you're trying to do, but hopefully this helps to simplify your implementation a bit.

Hi, @lucwillems, I faced with same issue and tried this for my token endpoint case(OAuth2ClientCredentialsAuthenticationConverter), but I have any idea how to use DelegatingAuthenticationConverter in my AuthorizationServerConfiguration. In which place can I use it in my SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http). Do you have a code example?

wsyours avatar Jun 09 '23 11:06 wsyours

The use case where the client does not request scope but is configured for authorization consent is invalid.

We about to migrate to the this new auth-server, and we have many service registered, who need to make the change define the scope when requesting service token (client_credential grant type). Is it possible to override this default behavior in case of service token?

aka-unk avatar Feb 15 '24 07:02 aka-unk