Skip to content
This repository was archived by the owner on Aug 26, 2026. It is now read-only.
This repository was archived by the owner on Aug 26, 2026. It is now read-only.

Support transforming authorized scopes when the OAuth2Authorization object is created #1504

Open

Description

@Kehrlann

Context

We have a use-case for filtering the scopes that go into an access_token, based on the Resource Owner's "roles" - e.g., if you have the role hr-user you can have payslip.view in the scopes of access tokens issued for you, but not the payslip.edit scope - even if the Client is allowed to request it.

There is no way to easily change the OAuth2Authorization#authorizedScopes() before it is created/saved.

The token itself, when it is a JWT, can be customized with an OAuth2TokenCustomizer<JwtEncodingContext> that acts on the scope claim, but the token response has the full list of authorized scopes.

Expected Behavior

When the OAuth2Authorization object is created and saved in the OAuth2Service, either through OAuth2AuthorizationCodeRequestAuthenticationProvider or OAuth2AuthorizationConsentAuthenticationProvider, I want to be able to alter the scopes.

Current workaround

Currently, we work around this by creating a custom AuthenticationProvider that wraps around both OAuth2AuthorizationCodeRequestAuthenticationProvider and OAuth2AuthorizationConsentAuthenticationProvider:

public class AppSsoAuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {

	// Either an OAuth2AuthorizationCodeRequestAuthenticationProvider or an OAuth2AuthorizationConsentAuthenticationProvider
	private final AuthenticationProvider delegate;

	private final OAuth2AuthorizationService authorizationService;

	public AppSsoAuthorizationCodeRequestAuthenticationProvider(AuthenticationProvider delegate,
			OAuth2AuthorizationService authorizationService) {
		this.delegate = delegate;
		this.authorizationService = authorizationService;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// This may throw an OAuth2AuthorizationCodeRequestAuthenticationException; in
		// this case we rethrow.
		var authResult = delegate.authenticate(authentication);

		// This does not happen with our supported cases, but in the case of the device
		// grant type, OAuth2AuthorizationConsentAuthenticationProvider will return null.
		if (authResult == null) {
			return null;
		}

		// When an authorization request comes in, and is valid, BUT the user is not
		// authenticated, an OAuth2AuthorizationCodeRequestAuthenticationToken is
		// returned, that is marked as !authenticated. This is a special case signaling
		// that the end-user must log-in first. In this case, we just follow through.
		//
		// The rest of the filter chain will save the incoming request in the session and
		// redirect the user to the login page. Once they are logged in, the saved request
		// will be replayed.
		if (!authResult.isAuthenticated()) {
			return authResult;
		}

		// Sometimes the authentication flow returns a
		// OAuth2AuthorizationConsentAuthenticationToken when consent is required.
		// In that case, we just follow through. Otherwise we grab the result.
		if (!(authResult instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authCodeAuthResult)) {
			return authResult;
		}

		// We load the authorization from the repo, change the scopes, and re-save it.
		var authCode = authCodeAuthResult.getAuthorizationCode();
		var authorization = authorizationService.findByToken(authCode.getTokenValue(),
				new OAuth2TokenType(OAuth2ParameterNames.CODE));

		// Filter the scopes based on the principal
		var filteredScopes = filterScopes(authorization.getAuthorizedScopes(), authResult.getPrincipal());

		var newAuthorization = OAuth2Authorization.from(authorization).authorizedScopes(filteredScopes).build();
		authorizationService.save(newAuthorization);

		//@formatter:off
		return new OAuth2AuthorizationCodeRequestAuthenticationToken(
				authCodeAuthResult.getAuthorizationUri(),
				authCodeAuthResult.getClientId(),
				authResultPrincipalAuthentication,
				authCodeAuthResult.getAuthorizationCode(),
				authCodeAuthResult.getRedirectUri(),
				authCodeAuthResult.getState(),
				filteredScopes
		);
		//@formatter:on
	}

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

	private Set<String> filterScopes(Set<String> authorizedScopes, Object principal) {
		// business logic
	}

}

Activity

  1. Suvink commented on Feb 2, 2024

    @Suvink

    @Kehrlann How did you register AppSsoAuthorizationCodeRequestAuthenticationProvider with the authorization server?

  2. Kehrlann commented on Feb 2, 2024

    @Kehrlann
    ContributorAuthor

    @Suvink excellent question, this is a classic pattern, but not 100% obvious. You register it with the AuthorizationServerConfigurer using an object post-processor. In your security configuration:

    @Configuration
    class SecurityConfiguration {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            return http
                    .authorizeHttpRequests(authorize -> {
                        // ...
                    })
                    // ...
                    .with(
                            new OAuth2AuthorizationServerConfigurer(),
                            authServer -> {
                                authServer.withObjectPostProcessor(new AuthorizationCodeAuthenticationProvider());
                                // ...
                            })
                    .build();
        }
    
        class AuthorizationCodeAuthenticationProvider implements ObjectPostProcessor<AuthenticationProvider> {
    
            @Override
            public <O extends AuthenticationProvider> O postProcess(O object) {
                if (object instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider) {
                    return (O) new AppSsoAuthorizationCodeRequestAuthenticationProvider(object, authorizationService);
                } else if (object instanceof OAuth2AuthorizationConsentAuthenticationProvider) {
                    return (O) new AppSsoAuthorizationCodeRequestAuthenticationProvider(object, authorizationService);
                }
                return object;
            }
    
        }
    }
  3. Suvink commented on Feb 7, 2024

    @Suvink

    Perfect. Initially we tried to register this with web security config but it didn't work. Then registered it with auth server configs and it works perfectly fine.
    Thanks a lot for the support!

  4. OrangeDog commented on Apr 11, 2025

    @OrangeDog

    The intended way to wrap the providers is:

    http
        // ...
        .with(authorizationServerConfigurer, config -> config
            .authorizationEndpoint(endpoint -> endpoint
                .authenticationProviders(providers -> providers
                    .replaceAll(provider -> new MyDelegatingProvider(provider))
        // ...

    Another way to solve this problem may be to filter the requested scopes in a custom AuthenticationConverter, but nothing will have been validated at that point.

  5. RazorNd commented on Oct 4, 2025

    @RazorNd

    Hi, I’ve been looking into this issue and I’d like to propose an API to address it.
    Currently, we can customize the scope claim in the token via OAuth2TokenCustomizer, but we cannot influence the set of authorizedScopes that is stored inside OAuth2Authorization. This creates inconsistencies when filtering scopes based on user roles or policies.

    Proposed API

    Introduce a functional interface:

    @FunctionalInterface
    public interface OAuth2ScopesAuthorizationFilter {
    
        Set<String> filter(OAuth2ScopesAuthorizationContext context);
    
        interface OAuth2ScopesAuthorizationContext {
            // Scopes as requested by the client (may be empty)
            Set<String> getRequestedScopes();
    
            // The client's allowed scopes: registeredClient.getScopes()
            Set<String> getClientAllowedScopes();
    
            // User or client principal (depending on grant type)
            Authentication getPrincipal();
    
            AuthorizationGrantType getGrantType();
    
            RegisteredClient getRegisteredClient();
    
            // Raw request params if needed by policy implementations
            Map<String, Object> getAdditionalParameters();
        }
    }

    Default implementation would simply return the requested scopes unchanged:

    (context) -> context.getRequestedScopes()

    Integration points

    Call the filter right before building/updating OAuth2Authorization:

    • OAuth2AuthorizationCodeRequestAuthenticationProvider (pre-consent and pre-persist)
    • OAuth2DeviceVerificationAuthenticationProvider (at user verification stage)
    • OAuth2ClientCredentialsAuthenticationProvider
    • (Optional) OAuth2RefreshTokenAuthenticationProvider — re-evaluate on refresh (down-scoping or policy changes)

    The filter’s return value becomes authorizedScopes() for the OAuth2Authorization.

    Benefits

    • Keeps authorizedScopes in sync with the actual policy decisions.
    • Allows role/tenant-based filtering without wrapping the whole provider.
    • Non-breaking: if no filter is configured, current behavior remains unchanged.

    Example

    @Bean
    OAuth2ScopesAuthorizationFilter roleBasedPolicy() {
        return ctx -> {
            boolean isAdmin = ctx.getPrincipal().getAuthorities().stream()
                    .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    
            // Always intersect with client-allowed scopes
            return ctx.getRequestedScopes().stream()
                .filter(ctx.getClientAllowedScopes()::contains)
                .filter(s -> !s.startsWith("admin:") || isAdmin)
                .collect(java.util.stream.Collectors.toUnmodifiableSet());
        };
    }

    Would this direction be acceptable for the project?
    If so, I’d be happy to work on an initial implementation and open a PR

  6. jgrandja commented on Oct 8, 2025

    @jgrandja
    Contributor

    @RazorNd Thanks for the suggestion. I'll take a look at this more closely after we release Spring Security 7.0 (Nov 17). There are higher priorities we are currently working on so this wouldn't make it into the next release.

  7. RazorNd commented on Jan 2, 2026

    @RazorNd

    Hi @jgrandja, just wanted to follow up on my proposal from October regarding OAuth2ScopesAuthorizationFilter. Now that Spring Security 7.0 has been released, have you had a chance to review it? I’d be glad to help with an initial implementation or open a PR if you think this approach is suitable. Thanks for your time!

  8. jgrandja commented on Jan 5, 2026

    @jgrandja
    Contributor

    @RazorNd I took a look at your proposed solution OAuth2ScopesAuthorizationFilter and I feel the interface addresses a narrow purpose - only deals with scopes.

    As per the main issue description:

    We have a use-case for filtering the scopes that go into an access_token, based on the Resource Owner's "roles" - e.g., if you have the role hr-user you can have payslip.view in the scopes of access tokens issued for you, but not the payslip.edit scope - even if the Client is allowed to request it.

    From a high-level design viewpoint, I believe the general abstraction here would be a Policy Decision. Having a general abstraction that allows for defining policies would provide a greater benefit since it could be used for different use cases, not just scope filtering. This would be my preference rather than introducing a new interface for a very specific use case.

    Furthermore, based on the 2x upvotes in the main issue description, I do wonder if this issue even needs to be addressed at this time as we do have many other issues that have more demand.

  9. Kehrlann commented on Feb 5, 2026

    @Kehrlann
    ContributorAuthor

    @jgrandja I've had a new case for transforming the OAuth2Authorization object before creating it.

    We perform additional actions during the authorization flow, and store the result in the authorization's pattern. In our case, we are fetching an OAuth2 token from an upstream authz server.

    Our current workaround is to use the base OAuth2AuthorizationCodeRequestAuthenticationProvider, then load the OAuth2Authorization from the service, modify it and save it again.

    It'd be really nice to have a Customizer<OAuth2Authorization.Builder> to modify the authorization in-place.

  10. OrangeDog commented on Feb 5, 2026

    @OrangeDog

    In addition to changing the scopes during the confirm/consent step, I am also recording additional metadata from the initial request in the OAuth2Authorization.

    Currently that's done in a custom OAuth2AuthorizationService.save(), grabbing stuff from RequestContextHolder if the attribute is missing. It's much less of a priority though, as it doesn't affect anything else, or need changing once first set, unlike scopes.

  11. jgrandja commented on Feb 18, 2026

    @jgrandja
    Contributor

    @Kehrlann

    It'd be really nice to have a Customizer<OAuth2Authorization.Builder> to modify the authorization in-place.

    I believe Customizer<OAuth2Authorization.Builder> would be limiting as I think we need to include additional context.

    For example, the following provide additonal context:

    OAuth2AuthorizationCodeRequestAuthenticationProvider -> Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator

    OAuth2AuthorizationConsentAuthenticationProvider -> Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer

    Ideally, we would define Consumer<OAuth2_XXX_AuthenticationContext> authorizationCustomizer that can be reused in OAuth2AuthorizationCodeRequestAuthenticationProvider and OAuth2AuthorizationConsentAuthenticationProvider or any AuthenticationProvider that calls OAuth2AuthorizationService.save().

  12. nikitanagar08 commented on Mar 2, 2026

    @nikitanagar08

    Hi, I'd like to work on this issue. I'll submit a draft PR with the implementation of the OAuth2ScopesAuthorizationFilter as proposed by @RazorNd. Will follow the integration points outlined in the discussion.

  13. added 2 commits that reference this issue on Mar 2, 2026
    2f9b48a
    6d2ecda
  14. jgrandja commented on Mar 3, 2026

    @jgrandja
    Contributor

    @nikitanagar08 You might have missed this comment

    @RazorNd I took a look at your proposed solution OAuth2ScopesAuthorizationFilter and I feel the interface addresses a narrow purpose - only deals with scopes.

    Your PR introduces OAuth2AuthorizedScopesMapper, which is not the enhancement we would be looking for. Please read the further comments. Also, at the moment, we are working on other higher priority items so our time is limited and I don't know if I have time to get to this in the current release cycle.

  15. added 2 commits that reference this issue on Mar 4, 2026
    2bbde2d
    517ed1e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions