Others Talk, We Listen.

Versioned, Validated and Secured REST Services with Spring 4.0

by Jens Alm on Mar 27, 2014

Part 6: Customising the Security for REST

In a series of blogs we will look at how to create a REST service using the latest version of Spring. Our service will have validation so we can make sure the data stored meets the requirements and it will be secured so only the correct people can make updates.

In the last entry we looked at basic configuration of the Spring Security Framework. In this final entry we will look at how to customize the security layer to be more aligned with our REST service.

Custom Authentication Entry Point

The authentication entry point is called when the client makes a call to a resource without proper authentication. In other words, the client has not logged in.

private class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
 
            response.setContentType("application/vnd.captech-v1.0+json");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            PrintWriter out = response.getWriter();
            out.print("{\"message\":\"Full authentication is required to access this resource.\", \"access-denied\":true,\"cause\":\"NOT AUTHENTICATED\"}");
            out.flush();
            out.close();
        }
    }

In the default Spring Security setup the request would be redirected to a login page of some sort. In a REST application we want to change that to just return a status of 401 (Unauthorized) and a static JSON message explaining it. We currently return the response as version 1 and we needed to handle a separate version we would need to look at the incoming headers. There is unfortunately no support for anything like @RequestMapping's produces and consumes.

Custom Authentication Success Handler

The authentication success handler is only called when the client successfully authenticates. In plain language that means when the client logs in.

private class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
 
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                            Authentication authentication) throws ServletException, IOException {
            try {
                String token = headerUtil.createAuthToken(((User) authentication.getPrincipal()).getUsername());
                ObjectMapper mapper = new ObjectMapper();
                ObjectNode node = mapper.createObjectNode().put("token", token);
                response.setContentType("application/vnd.captech-v1.0+json");
                PrintWriter out = response.getWriter();
                out.print(node.toString());
                out.flush();
                out.close();
            } catch (GeneralSecurityException e) {
                throw new ServletException("Unable to create the auth token", e);
            }
            clearAuthenticationAttributes(request);
 
        }
    }

The default Spring Security implementation would add some information to the session. The session is identified by a cookie that is sent back and forth for each request and response. After adding the information to the session Spring Security would then redirect the client to the page they originally requested or a similar page.
Instead of this behavior we start by creating an encrypted token by calling HeaderUtil. This token contains the username and the timestamp. We return a the JSON containing the token but we don’t have to specify a status code since the default is 200 (OK). This keeps our application stateless.

Custom Access Denied Handler

The access denied handler is called when authorization fails. This means the client is passing in a correct token but the permissions associated with the role of this user does not allow the client the access.

private class CustomAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
 
            response.setContentType("application/vnd.captech-v1.0+json");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            out.print("{\"message\":\"You are not privileged to request this resource.\", \"access-denied\":true,\"cause\":\"AUTHORIZATION_FAILURE\"}");
            out.flush();
            out.close();
 
        }
    }

The default implementation would redirect to a access denied page but in our REST implementation we need to return a status of 403 (Forbidden) and a message.

Adding It All Together

To customise the default behaviour we need to override the configure() method of the WebSecurityConfigurerAdapter which we extended in SecurityConfig.

@Override
protected void configure(HttpSecurity http) throws Exception {
 
     http.
             addFilterBefore(authenticationFilter(), LogoutFilter.class).
 
             csrf().disable().
 
             formLogin().successHandler(new CustomAuthenticationSuccessHandler()).
             loginProcessingUrl("/login").
 
             and().
 
             logout().
             logoutSuccessUrl("/logout").
 
             and().
 
             sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
 
             and().
 
             exceptionHandling().
             accessDeniedHandler(new CustomAccessDeniedHandler()).
             authenticationEntryPoint(new CustomAuthenticationEntryPoint()).
 
             and().
 
             authorizeRequests().
             antMatchers(HttpMethod.POST, "/login").permitAll().
             antMatchers(HttpMethod.POST, "/logout").authenticated().
             antMatchers(HttpMethod.GET, "/**").hasRole("USER").
             antMatchers(HttpMethod.POST, "/**").hasRole("ADMIN").
             antMatchers(HttpMethod.DELETE, "/**").hasRole("ADMIN").
             anyRequest().authenticated();
 
    }

Let’s examine the configure method step by step.

     addFilterBefore(authenticationFilter(), LogoutFilter.class)

This adds our new HeaderAuthenticationFilter to the chain of filters that process each request to the application. We add it before the LogoutFilter so it will be the first filter in the chain.

     csrf().disable().

This disables the built in Cross Site Request Forgery support. This is used in a html login form but since we don’t have that we need to disable this support.

formLogin().successHandler(successHandler).
     loginProcessingUrl("/login").
 
     and().
 
     logout().
     logoutSuccessUrl("/logout").

Adds our success handler and maps the login and logout filters to their respective endpoints.

sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).

This tells the application not to create sessions in keeping with our stateless application.

exceptionHandling().
     accessDeniedHandler(new CustomAccessDeniedHandler()).
     authenticationEntryPoint(new CustomAuthenticationEntryPoint()).

This adds our custom handlers for authententication and authorization.

authorizeRequests().
     antMatchers(HttpMethod.POST, "/login").permitAll().
     antMatchers(HttpMethod.POST, "/logout").authenticated().
     antMatchers(HttpMethod.GET, "/**").hasRole("USER").
     antMatchers(HttpMethod.POST, "/**").hasRole("ADMIN").
     antMatchers(HttpMethod.PUT, "/**").hasRole("ADMIN").
     antMatchers(HttpMethod.PATCH, "/**").hasRole("ADMIN").
     antMatchers(HttpMethod.DELETE, "/**").hasRole("ADMIN").
     anyRequest().authenticated();

This is the heart of our security. This decides what requests should require what role. In our example we are letting any POST request to “/login” through by using the permitAll().
For the “/logout” we require that the request is correctly authenticated but we don’t care what role the user has.
For all our other matchers we use wildcards but specify the request method. As a user you only get view permissions using GET and as admin you get all the update methods POST, PUT, PATCH and DELETE.

Conclusion

This concludes the series and we now have a versioned, validated and secured REST service. It may not be HATOAS but it is completely stateless. We accomplished this using only three xml files in total, two of which are for dozer and the third being the logback configuration. Not a single Spring xml file as far as the eye can see.
Building an app or a web frontend using Angular.js or JQuery to consume these services I will leave for someone else more suited to that purpose to demonstrate.

Each part of this blog has added functionality to our application.
Part 1: Creating the Basic Application
Part 2: Configuring The Application
Part 3: Adding the REST Services
Part 4: Adding Validation
Part 5: Adding Security to the Services
Part 6: Customising the Security for REST

The code examples are all heavily edited excerpts but the complete source code can be found on GitHub.