Skip to content
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

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

Open
Kehrlann opened this issue Jan 12, 2024 · 3 comments
Labels
type: enhancement A general enhancement

Comments

@Kehrlann
Copy link
Contributor

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
	}

}
@Kehrlann Kehrlann added the type: enhancement A general enhancement label Jan 12, 2024
@Suvink
Copy link

Suvink commented Feb 2, 2024

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

@Kehrlann
Copy link
Contributor Author

Kehrlann commented Feb 2, 2024

@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;
        }

    }
}

@Suvink
Copy link

Suvink commented Feb 7, 2024

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!

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
Development

No branches or pull requests

2 participants