We have discussed a lot on spring security and created multiple sample examples on it. You can check all these articles here - Spring Security Tutorials. In this article, we will discuss exception handling in spring security. To do so we will implement the interface AuthenticationEntryPoint to handle the authentication exception and implement AccessDeniedHandler interface to handle access denied exception i.e. Forbidden(403).
Exception Handling in Web Security
To handle REST exception, we generally use @ControllerAdvice
and @ExceptionHandler
in Spring MVC but these handler works if the request is handled by the DispatcherServlet. However, security-related exceptions occur before that as it is thrown by Filters. Hence, it is required to insert a custom filter (RestAccessDeniedHandler and RestAuthenticationEntryPoint) earlier in the chain to catch the exception and return accordingly. These filters can be inserted easily in our web security configurations. You can check my previous article - Spring Security for an end-to-end spring security app. Below is a sample WebSecurityConfig defined but without any exception handling filters.
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(encoder()); } @Bean public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/token/*", "/signup").permitAll() .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); }
Now, to add our exception handling filter, we can customise configure()
and add it here. Below is the implemntation of configure()
that adds exception handling filetrs.
@Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/token/*", "/signup").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); }
Let us define our RestAccessDeniedHandler
and RestAuthenticationEntryPoint
now.
package com.devglan.exception; import com.devglan.model.ApiResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ApiResponse response = new ApiResponse(401, "Unauthorised"); response.setMessage("Unauthorised"); OutputStream out = httpServletResponse.getOutputStream(); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(out, response); out.flush(); } }RestAccessDeniedHandler
package com.devglan.exception; import com.devglan.model.ApiResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; @Component public class RestAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { ApiResponse response = new ApiResponse(403, "Access Denied"); response.setMessage("Access Denied"); OutputStream out = httpServletResponse.getOutputStream(); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(out, response); out.flush(); } }
Here, we have a generic response of our API i.e. ApiResponse and hence even if we have any exception the API response will be same and the client code remains the same in success and failure case. Below is the ApiResponse
package com.devglan.model; public class ApiResponse { private int status; private String message; private Object result; public ApiResponse(int status, String message, Object result){ this.status = status; this.message = message; this.result = result; } public ApiResponse(int status, String message){ this.status = status; this.message = message; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } @Override public String toString() { return "ApiResponse [statusCode=" + status + ", message=" + message +"]"; } }
Exception Handling in OAUTH2
In an OAUTH2 protocol, Resource is the REST API which we have exposed for the crud operation. To access these resources, the client must be authenticated. In real-time scenarios, whenever a user tries to access these resources, the user will be asked to provide his authenticity and once the user is authorized then he will be allowed to access these protected resources. But for an unauthorised user there should be an exception thrown. For an end-to-end spring boot security OAUTH2 app, you can visit here - spring boot security OAUTH2 app
Now, to define our custom exception handling in OAUTH2, we can inert our custom defined exception handling filters (RestAccessDeniedHandler and RestAuthenticationEntryPoint) in the resource server configuration. Below is the resource server config that insert custom class for exception handling.
ResourceServerConfig.javapackage com.devglan.config; import com.devglan.exception.RestAccessDeniedHandler; import com.devglan.exception.RestAuthenticationEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "resource_id"; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID).stateless(false); } @Override public void configure(HttpSecurity http) throws Exception { http. anonymous().disable() .authorizeRequests() .antMatchers("/users/**").access("hasRole('ADMIN')") .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler()).authenticationEntryPoint(authenticationEntryPoint()); } @Bean RestAccessDeniedHandler accessDeniedHandler() { return new RestAccessDeniedHandler(); } @Bean RestAuthenticationEntryPoint authenticationEntryPoint() { return new RestAuthenticationEntryPoint(); } }