11

I'm trying to make my Spring Boot 1.5.x REST API project 2.x.x-compatible, without breaking lots of code. I'm stuck in a filter-based JWT Spring Security implementation I used to use:

In order to authenticate credentials through "/login" endpoint, I used to extend UsernamePasswordAuthenticationFilter as follows:

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    private JWTUtil jwtUtil;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {

        try {
            CredenciaisDTO creds = new ObjectMapper().readValue(req.getInputStream(), CredenciaisDTO.class);
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(creds.getEmail(), creds.getSenha(), new ArrayList<>());
            Authentication auth = authenticationManager.authenticate(authToken);
            return auth;
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        String username = ((UserSS) auth.getPrincipal()).getUsername();
        String token = jwtUtil.generateToken(username);
        res.addHeader("Authorization", "Bearer " + token);
        res.addHeader("access-control-expose-headers", "Authorization");
    }
}

Also, in order to authorize beared-token requests, I used to extend BasicAuthenticationFilter as follows:

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    private JWTUtil jwtUtil;

    private UserDetailsService userDetailsService;

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil, UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            UsernamePasswordAuthenticationToken auth = getAuthentication(header.substring(7));
            if (auth != null) {
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(String token) {
        if (jwtUtil.isValid(token)) {
            String username = jwtUtil.getUsername(token);
            UserDetails user = userDetailsService.loadUserByUsername(username);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        }
        return null;
    }
}

Everything worked as expected:

  • Requests to /login with bad credentials used to return 401 with a standand "Unauthorized/Authentication failed: bad credentials" object.

  • Beared-token requests not authorized by BasicAuthenticationFilter used to return 403 with a standard "Forbidden/Access denied" object.

However, if I use that code in a Spring Boot 2.0.0 project, requests do /login were returning 403 with empty-body response.

This post adviced to include http.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); in configure method from the security configuration class. That way I could get 401 for /login requests, but it's still returning an empty-body response (instead of that standard "Unauthorized" error object). Besides, now all requests not authorized by BasicAuthenticationFilter are also returning 401 with empty-body response (while the correct should be returning 403 with that standard "Forbidden" error object inside body).

How can I get back that desirable behavior I had before?

Nelio Alves
  • 1,231
  • 13
  • 34
  • Just to double-check, Spring Boot 2.0.0 turns on csrf protection by default, which is different from previous versions. If you don't have csrf explicitly enabled or disabled, then that behavior likely changed in your upgrade. – jzheaux Mar 26 '18 at 18:00

1 Answers1

9

Got it. The answer from here was not necessary. In order to solve my problem I've implemented an AuthenticationFailureHandler:

public class JWTAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        response.setStatus(401);
        response.setContentType("application/json"); 
        response.getWriter().append(json());
    }

    private String json() {
        long date = new Date().getTime();
        return "{\"timestamp\": " + date + ", "
            + "\"status\": 401, "
            + "\"error\": \"Unauthorized\", "
            + "\"message\": \"Authentication failed: bad credentials\", "
            + "\"path\": \"/login\"}";
    }
}

And then inject it in UsernamePasswordAuthenticationFilter:

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    (...)

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {
        super.setAuthenticationFailureHandler(new JWTAuthenticationFailureHandler());
        (...)
Nelio Alves
  • 1,231
  • 13
  • 34