Skip to content

OAuth2 authorization code flow, support parameters when request access token. #10452

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
chenrujun opened this issue Oct 29, 2021 · 5 comments
Closed
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid

Comments

@chenrujun
Copy link
Contributor

Background

In Microsoft identity platform and OAuth 2.0 authorization code flow:

  1. When request an authorization code, the scope can cover multiple resources. For example: resource-1.scope-1, resource-1.scope-2, resource-2.scope-1, resource-2.scope-2.
  2. When request an access token, scope is optional. scope must all be from single resource. So if authorization code's scope cover multiple resources, scope can not be omitted when request an access token. Otherwise it will have error like this: [invalid_request] AADSTS28003: Provided value for the input parameter scope cannot be empty when requesting an access token using the provided authorization code. Please specify a valid scope. Trace ID: <UUID> Correlation ID: <UUID> Timestamp: <Timestamp>.
  3. Check https://github.com/MicrosoftDocs/azure-docs/issues/82875 to get more information why Microsoft identity platform(Azure Active Directory)'s access token's scope must all be from single resource. (access token only have one audience.)

Current problem

In current implementation of OAuth2AuthorizationCodeGrantRequestEntityConverter, it does not support add scope parameter when request for an access token.

Implementation suggestion

  1. Add a map field in ClientRegistration, for example: accessTokenParameters:
public final class ClientRegistration implements Serializable {
	...
	private Map<String, Object> accessTokenParameters= Collections.emptyMap();
        ...
}
  1. Use clientRegistration.getAccessTokenParameters in OAuth2AuthorizationCodeGrantRequestEntityConverter.
@chenrujun chenrujun added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Oct 29, 2021
@marcusdacoregio marcusdacoregio added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 29, 2021
@jgrandja
Copy link
Contributor

jgrandja commented Nov 2, 2021

@chenrujun What version of Spring Security are you using?

In current implementation of OAuth2AuthorizationCodeGrantRequestEntityConverter, it does not support add scope parameter when request for an access token.

The current implementation of OAuth2AuthorizationCodeGrantRequestEntityConverter provides the ability to customize the token request parameters via setParametersConverter(), addParametersConverter() or you can override createParameters(). Please see the documentation for further details.

I'm going to close this as customizing the token request parameters is possible with the current implementation.

@jgrandja jgrandja closed this as completed Nov 2, 2021
@jgrandja jgrandja added status: invalid An issue that we don't feel is valid and removed type: bug A general bug labels Nov 2, 2021
@chenrujun
Copy link
Contributor Author

chenrujun commented Nov 2, 2021

@jgrandja

Thank you for your response.

The current implementation of OAuth2AuthorizationCodeGrantRequestEntityConverter provides the ability to customize the token request parameters via setParametersConverter(), addParametersConverter() or you can override createParameters(). Please see the documentation for further details.

I know current method. But if customer have multiple client-registrations, and each client-registration need it's own parameter when request for access token. It is troublesome to use current implementation:

1. Current implementation:

1.1. In application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          client-1:
            # ...
          client-2:
            # ...
          client-3:
          # ...

1.2. In self-defined converter:

public class AzureActiveDirectoryOAuth2AuthorizationCodeGrantRequestEntityConverter extends OAuth2AuthorizationCodeGrantRequestEntityConverter {

    @Override
    protected MultiValueMap<String, String> createParameters(
        OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        MultiValueMap<String, String> parameters = super.createParameters(authorizationCodeGrantRequest);
        String id = authorizationCodeGrantRequest.getClientRegistration().getRegistrationId();
        if (id.equals("client-1")) {
            parameters.add("scopes", "scope-1");
        } else if (id.equals("client-2")) {
            parameters.add("scopes", "scope-1");
        } else if (id.equals("client-3")) {
            parameters.add("scopes", "scope-1");
        }
        return parameters;
    }
}

2. What I wanted:

2.1. In application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          client-1:
            additional-parameters-when-request-for-access-token:
              scope: scope-1
            # ...
          client-2:
            additional-parameters-when-request-for-access-token:
              scope: scope-2
            # ...
          client-3:
            additional-parameters-when-request-for-access-token:
              scope: scope-2
          # ...

2.2. In OAuth2AuthorizationCodeGrantRequestEntityConverter.java:

public class OAuth2AuthorizationCodeGrantRequestEntityConverter
		extends AbstractOAuth2AuthorizationGrantRequestEntityConverter<OAuth2AuthorizationCodeGrantRequest> {

    @Override
    protected MultiValueMap<String, String> createParameters(
        OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
        OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
        parameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
        String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
        String codeVerifier = authorizationExchange.getAuthorizationRequest()
                                                   .getAttribute(PkceParameterNames.CODE_VERIFIER);
        if (redirectUri != null) {
            parameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
        }
        if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
            && !ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
            parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
        }
        if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())
            || ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
            parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
        }
        if (codeVerifier != null) {
            parameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);
        }
// Add codes from here
        MultiValueMap<String, String> additionalParameters = clientRegistration.getAdditionalParametersWhenRequestForAccessToken();
        parameters.addAll(additionalParameters);
        return parameters;
    }

}

By doing this, developer using spring-security can add additionalParametersWhenRequestForAccessToken much easier: Just add properties in application.yml, no need to add any java code.

@chenrujun
Copy link
Contributor Author

BTW, if ClientRegistration add a field named additionalParametersWhenRequestForAccessToken in , we need update all converters, for example: JwtBearerGrantRequestEntityConverter.

After JwtBearerGrantRequestEntityConverter add logic about additionalParametersWhenRequestForAccessToken, I can delete a lot of java codes in my current sample:

  1. AzureADJwtBearerGrantRequestEntityConverter
public class AzureADJwtBearerGrantRequestEntityConverter extends JwtBearerGrantRequestEntityConverter {

    @Override
    protected MultiValueMap<String, String> createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) {
        MultiValueMap<String, String> parameters = super.createParameters(jwtBearerGrantRequest);
        parameters.add("requested_token_use", "on_behalf_of");
        return parameters;
    }

}
  1. ApplicationConfiguration
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {
        OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                                                 .provider(jwtBearerOAuth2AuthorizedClientProvider())
                                                 .build();
        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        return authorizedClientManager;
    }

    private JwtBearerOAuth2AuthorizedClientProvider jwtBearerOAuth2AuthorizedClientProvider() {
        JwtBearerOAuth2AuthorizedClientProvider provider = new JwtBearerOAuth2AuthorizedClientProvider();
        provider.setAccessTokenResponseClient(oAuth2AccessTokenResponseClient());
        return provider;
    }

    private DefaultJwtBearerTokenResponseClient oAuth2AccessTokenResponseClient() {
        DefaultJwtBearerTokenResponseClient client = new DefaultJwtBearerTokenResponseClient();
        client.setRequestEntityConverter(new AzureADJwtBearerGrantRequestEntityConverter());
        return client;
    }

@jgrandja
Copy link
Contributor

jgrandja commented Nov 4, 2021

@chenrujun I'm not keen on adding the property additional-parameters-when-request-for-access-token.

Firstly, the scope parameter is part of the Authorization Request NOT Token Request for the authorization_code grant. This is non-standard behaviour and is specific to Microsoft's implementation. Our goal is to implement to spec only and provide hooks for customization including non-standard behaviour. Hence, the Converter hook is provided and your implementation AzureActiveDirectoryOAuth2AuthorizationCodeGrantRequestEntityConverter is the right approach.

Also, the additional-parameters-when-request-for-access-token is quite limiting as it would only support static parameters. The Converter has the benefit of supporting dynamic parameters as well.

@chenrujun
Copy link
Contributor Author

@jgrandja

I got it. Thank you for your response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

3 participants