terminal
terminal

Reputation: 125

Custom message in Spring AccessDeniedException

In my Spring Boot Web application I am throwing a org.springframework.security.access.AccessDeniedException in my business code whenever a user does not have a necessary authority. The AccessDeniedException has the advantage that Spring automatically calls the AuthenticationEntryPoint etc.

However, I am using a custom exception message to indicate the exact cause of the exception (e.g. throw new AccessDeniedException("You do not have permissions to do XYZ"), but in the generated JSON response the message is always "Access Denied", e.g.:

{
  "timestamp": "2019-12-12T10:01:10.342+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/myurl"
}

Digging into the code of Spring (DefaultErrorAttributes method addErrorDetails) reveals that the message is built from 1) the exception message 2) the request attribute javax.servlet.error.message, with the second overriding the first.

Does anybody know why the request attribute has priority over the exception message?

And is there an easy way to display the custom exception message within the Spring Boot error response? I would like to retain the default Spring Boot error response, but with my custom message in the "message" field and ideally I want Spring Boot to construct the rest of the JSON object (i.e. timestamp, status, etc). The final result should look something like this:

{
  "timestamp": "2019-12-12T10:01:10.342+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "You do not have permissions to do XYZ",
  "path": "/myurl"
}

This does work for other kinds of exceptions. Just AccessDeniedException seems to be a special case. Is AccessDeniedException maybe not the right choice for this rqeuirement (although the javadoc suggests it is)?

Upvotes: 4

Views: 9250

Answers (2)

T A
T A

Reputation: 1756

You could use Springs ControllerAdvice and send a custom error using the HttpServletResponse:

@ControllerAdvice
class AccessDeniedExceptionHandler {

    @ExceptionHandler(value = AccessDeniedException.class)
    public void handleConflict(HttpServletResponse response) throws IOException {
        response.sendError(403, "Your Message");
    }
}

This will return:

{
  "timestamp": 1576248996366,
  "status": 403,
  "error": "Forbidden",
  "message": "Your Message",
  "path": "/foo/bar"
}

This way you can globally catch all Exceptions of a specific type and make your controller return a specified custom value. You could do so as well e.g. for any IOException and return a 404 Not Found ResponseEntity with a message you specify.

Upvotes: 0

Ashok Kumar N
Ashok Kumar N

Reputation: 573

place the method inside the implementation class AccessDeniedHandlerImpl with AccessDeniedHandler interface

    @Override
public void handle(HttpServletRequest request, HttpServletResponse response,
                   AccessDeniedException accessDeniedException) throws IOException,
        ServletException {

    ObjectMapper mapper = new ObjectMapper();
    JsonObject jsonObject=new JsonObject();
    jsonObject.add("timestamp", Instant.now().toString());
    jsonObject.add("status", HttpStatus.FORBIDDEN.value());
    jsonObject.add("error", HttpStatus.FORBIDDEN.getReasonPhrase());
    jsonObject.add("message","********* custom message what you want ********");
    jsonObject.add("path", request.getRequestURI());

    response.setStatus(HttpStatus.FORBIDDEN.value());
    response.getWriter().write( mapper.writeValueAsString(jsonObject));
}

try this if you have a security configuration like below and

add the AccessDeniedHandlerImpl as default accessDeniedHandler

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/css/**", "/index").permitAll()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            .and()
        .formLogin()
            .and()
        .exceptionHandling()
            .accessDeniedHandler((request, response, accessDeniedException) -> {
                AccessDeniedHandler defaultAccessDeniedHandler = new AccessDeniedHandlerImpl();
                defaultAccessDeniedHandler.handle(request, response, accessDeniedException); // handle the custom acessDenied class here
            });
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .inMemoryAuthentication()
            .withUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER"))
            .withUser(User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN"));
}
}

Upvotes: 3

Related Questions