I am working on a Spring Boot & Spring Security application that makes use of JSON Web Tokens.
I have a spring security filter that checks for the presence of an existing JWT and if so, injects a UsernamePasswordAuthenticationToken:
public class AuthenticationTokenFilter extends UsernamePasswordAuthenticationFilter {
@Value("${api.token.header}")
String tokenHeader;
@Autowired
TokenUtility tokenUtility;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
String incomingToken = httpRequest.getHeader(tokenHeader);
if (SecurityContextHolder.getContext().getAuthentication() == null && incomingToken != null) {
UserDetails userDetails = null;
try {
userDetails = tokenUtility.validateToken(incomingToken);
} catch (TokenExpiredException e) {
throw new ServletException("Token has expired", e);
}
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
This filter is injected as follows:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Autowired
EntryPointUnauthorizedHandler unauthorizedHandler;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public AuthenticationTokenFilter authenticationTokenFilter() throws Exception {
AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManager());
return authenticationTokenFilter;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
// filter injected here
httpSecurity.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
If a user passes in a token that has expired, they receive the following error:
{
"timestamp":1496424964894,
"status":500,
"error":"Internal Server Error",
"exception":"com.app.exceptions.TokenExpiredException",
"message":"javax.servlet.ServletException: Token has expired",
"path":"/orders"
}
I know that spring security intercepts the requests before they make it to the controller layer, so I can't use my existing @ControllerAdvice to handle these exceptions.
My question is, how do I customise the error message/object that gets returned here? Elsewhere I use a JSON-serialized POJO to return error messages and I want to be consistent. I also don't want the user to see javax.servlet.ServletException
First, modify JWTTokenProvider Class to add a custom header to Http Servlet Request using setAttribute() method.
public boolean validateToken(String token,HttpServletRequest httpServletRequest){
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
}catch (SignatureException ex){
System.out.println("Invalid JWT Signature");
}catch (MalformedJwtException ex){
System.out.println("Invalid JWT token");
}catch (ExpiredJwtException ex){
System.out.println("Expired JWT token");
httpServletRequest.setAttribute("expired",ex.getMessage());
}catch (UnsupportedJwtException ex){
System.out.println("Unsupported JWT exception");
}catch (IllegalArgumentException ex){
System.out.println("Jwt claims string is empty");
}
return false;
}
Then modify commence method in JwtAuthenticationEntryPoint class to check expired header in http servlet request header that we added above.
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
final String expired = (String) httpServletRequest.getAttribute("expired");
System.out.println(expired);
if (expired!=null){
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,expired);
}else{
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Invalid Login details");
}
}
For more details see this Post. A nice simple solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With