Expected Behavior

I want to customize the AuthorizationManager and the AccessDeniedException message.

Current Behavior

The error message is always set to Access Denied

Context

I want to customize the AccessDeniedException message, but I found that the AuthorizationManager calls the check method instead of the verify method, which leads to repeated logic and overwrites the information.

// AuthorizationFilter
try {
    AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
    this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
    if (decision != null && !decision.isGranted()) {
        throw new AccessDeniedException("Access Denied");
    }
    chain.doFilter(request, response);
}
finally {
    request.removeAttribute(alreadyFilteredAttributeName);
}

Comment From: marcusdacoregio

Hi, @ion1ze. Is there something you want to achieve that you cannot do by customizing the ExceptionTranslationFilter?

Comment From: marcusdacoregio

This is probably a duplicate of https://github.com/spring-projects/spring-security/issues/9823

Comment From: ion1ze

This is probably a duplicate of #9823

I want to handle various types of AccessDeniedException, such as those caused by the tenant or by the role. In each case, I need to return a different message.

Comment From: marcusdacoregio

Can you elaborate more on how you apply those authorization checks for tenants/role and if you could customize the message, how do you imagine doing that?

Comment From: ion1ze

Can you elaborate more on how you apply those authorization checks for tenants/role and if you could customize the message, how do you imagine doing that?

  1. Create a new AuthorizationManager,and override the verify and check method
public class TenantAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationDecision decision = check(authentication, object);
        if (decision != null && !decision.isGranted()) {
            // I want to customize the exception message
            throw new AccessDeniedException("Access Denied");
        }
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        Long expectedTenantId = this.obtainTenantId(authentication);
        Long tenantId = TenantContextHolder.getRequiredTenantId();
        boolean granted = expectedTenantId.equals(tenantId);
        if (!granted) {
            log.warn("Tenant mismatch, expected {} actually {}",expectedTenantId,tenantId);
        }
        return new TenantAuthorizationDecision(granted, tenantId);
    }

    private Long obtainTenantId(Supplier<Authentication> authentication) {
        return Optional.ofNullable(authentication.get())
                .map(Authentication::getPrincipal)
                .map(Jwt.class::cast)
                .map((source) -> Long.parseLong(source.getClaimAsString("tenantId")))
                .orElseThrow(() -> new BadCredentialsException("Obtain tenant id failure."));
    }
}
  1. Use the AuthorizationManager
 @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                   JwtEncoder jwtEncoder,
                                                   TenantAuthenticationFilter tenantAuthenticationFilter) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((configurer) -> {
                    configurer.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll();
                    configurer.requestMatchers(this.properties.getPermitAllAntPatterns()).permitAll();
                    configurer.requestMatchers("/**").access(new TenantAuthorizationManager());
                    configurer.anyRequest().authenticated();
                })
 // ...
  1. The AuthorizationFilter call the verify method not the check method.
// AuthorizationFilter
try {
        // The repeated logic 
    // AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
    // this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
    // if (decision != null && !decision.isGranted()) {
    //  throw new AccessDeniedException("Access Denied");
    // }
        AuthorizationDecision decision = this.authorizationManager.verify(this::getAuthentication, request);
        // this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);  // There seems to be a bit of trouble here
    chain.doFilter(request, response);
}
finally {
    request.removeAttribute(alreadyFilteredAttributeName);
}
  1. Thank you for your answer. Suddenly realize my suggestion seems to go against the design of Spring Security.