How can I get my custom ResponseEntityExceptionHandler
or OAuth2ExceptionRenderer
to handle Exceptions raised by Spring security on a pure resource ser
OAuth2ExceptionRenderer is for an Authorization Server. The correct answer is likely to handle it like detailed in this post (that is, ignore that it's oauth and treat it like any other spring security authentication mechanism): https://stackoverflow.com/a/26502321/5639571
Of course, this will catch oauth related exceptions (which are thrown before you reach your resource endpoint), but any exceptions happening within your resource endpoint will still require an @ExceptionHandler method.
As noted in previous comments the request is rejected by the security framework before it reaches the MVC layer so @ControllerAdvice
is not an option here.
There are 3 interfaces in the Spring Security framework that may be of interest here:
You can create implementations of each of these Interfaces in order to customize the response sent for various events: successful login, failed login, attempt to access protected resource with insufficient permissions.
The following would return a JSON response on unsuccessful login attempt:
@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException, ServletException
{
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> data = new HashMap<>();
data.put("timestamp", new Date());
data.put("status",HttpStatus.FORBIDDEN.value());
data.put("message", "Access Denied");
data.put("path", request.getRequestURL().toString());
OutputStream out = response.getOutputStream();
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, data);
out.flush();
}
}
You also need to register your implementation(s) with the Security framework. In Java config this looks like the below:
@Configuration
@EnableWebSecurity
@ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
@Override
public void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
.logout()
.deleteCookies("JESSIONID")
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}
/**
* @return Custom {@link AuthenticationFailureHandler} to send suitable response to REST clients in the event of a
* failed authentication attempt.
*/
@Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
/**
* @return Custom {@link AuthenticationSuccessHandler} to send suitable response to REST clients in the event of a
* successful authentication attempt.
*/
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler()
{
return new RestAuthenticationSuccessHandler();
}
/**
* @return Custom {@link AccessDeniedHandler} to send suitable response to REST clients in the event of an attempt to
* access resources to which the user has insufficient privileges.
*/
@Bean
public AccessDeniedHandler accessDeniedHandler()
{
return new RestAccessDeniedHandler();
}
}
Spring 3.0 Onwards,You can use @ControllerAdvice
(At Class Level) and extends org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
class from CustomGlobalExceptionHandler
@ExceptionHandler({com.test.CustomException1.class,com.test.CustomException2.class})
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex){
return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);
}
If you're using token validation URL with config similar to Configuring resource server with RemoteTokenServices in Spring Security Oauth2 which returns HTTP status 401 in case of unauthorized:
@Primary
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
return tokenService;
}
Implementing custom authenticationEntryPoint
as described in other answers (https://stackoverflow.com/a/44372313/5962766) won't work because RemoteTokenService use 400 status and throws unhandled exceptions for other statuses like 401:
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
@Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
So you need to set custom RestTemplate
in RemoteTokenServices
config which would handle 401 without throwing exception:
@Primary
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
RestOperations restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
@Override
// Ignore 400 and 401
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
}
tokenService.setRestTemplate(restTemplate);
return tokenService;
}
And add dependency for HttpComponentsClientHttpRequestFactory:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
We can use this security handler to pass the handler to spring mvc @ControllerAdvice
@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);
private final HandlerExceptionResolver resolver;
@Autowired
public AuthExceptionHandler(@Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOG.error("Responding with unauthorized error. Message - {}", authException.getMessage());
resolver.resolveException(request, response, null, authException);
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("Responding with access denied error. Message - {}", accessDeniedException.getMessage());
resolver.resolveException(request, response, null, accessDeniedException);
}
}
Then define the exception by using @ControllerAdvice
so that we can manage the global exception handler in one place..
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse res,
AuthenticationException authException) throws IOException, ServletException {
ApiException ex = new ApiException(HttpStatus.FORBIDDEN, "Invalid Token", authException);
ObjectMapper mapper = new ObjectMapper();
res.setContentType("application/json;charset=UTF-8");
res.setStatus(403);
res.getWriter().write(mapper.writeValueAsString(ex));
}
}