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?
- 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."));
}
}
- 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();
})
// ...
- 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);
}
- Thank you for your answer. Suddenly realize my suggestion seems to go against the design of Spring Security.