Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot 2 - 403 instead of 401 in filter-based JWT Spring Security implementation

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?

like image 809
Nelio Alves Avatar asked Mar 26 '18 17:03

Nelio Alves


1 Answers

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());
        (...)
like image 198
Nelio Alves Avatar answered Nov 15 '22 06:11

Nelio Alves