loading...

Java - secure an endpoint using Spring Security

brunooliveira profile image Bruno Oliveira ・12 min read

Introduction

In one of the previous posts, we created a simple domain consisting of a Person and a Greet class, and we modelled a basic endpoint accepting POST requests.

When we designed the endpoint, there were no security concerns, meaning that anyone could access our endpoint, perform a request and get a response back. In real-world scenarios, this is often not sufficient and we need to have security implemented so that not everyone can access our endpoints. We'll go over two main aspects of security: authentication and authorization.

Authentication and authorization

Spring offers support for security via Spring Security. We can just add it to our POM file or choose it as a dependency via Spring initializr, and we can start using the security features Spring offers us in our code.

These are example dependencies for security found in some production code using Spring:

 <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>${spring-security.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>${spring-security.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>${spring-security.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <version>${spring-security.version}</version>
        </dependency>

obviously, these can all be adjusted to your needs.

The difference between authentication and authorization is important from a functional perspective, and, in most cases, configuration added in the form of annotations and/or writing custom EntryPoints, by extending from BasicAuthenticationEntryPoint and implementing the AuthenticationEntryPoint interface are all that is needed to ensure both mechanisms are translated into code.

Let's see the differences between both:

  • Authentication: concerned with the idea of validating credentials and by extension, if we are who we say we are. If my username is user1 and my password is pass1, then, only if both of these credentials are entered correctly, I can be seen as who I claim to be.

  • Authorization: in Spring, authorizations are intimately tied to ROLES, and a simple analogy can be one between a standard user and an admin. The admin will have the privilege to delete other user accounts for example, while a standard user will be able to switch their password for example. After me being who I say I am, what can I do?

Introduction to Spring Security by example

Spring Security is a vast and complex topic on its own, so, the aspects discussed here are a fraction of what Spring Security can do.

We will secure our endpoint using Basic Authentication, meaning that a user will need to enter a username and a password to be able to perform a request. As an example slightly closer to what could be a real scenario, we will externalize our configuration and for the purpose of this example, we will read in the user credentials from the application.properties file.

Externalizing the configuration is always a good idea, because it means that the logic for the authentication can always remain independent of the code which is desirable, and we can then get the credentials from a file, store them in-memory, retrieve them from a database, etc.

Preparing the security configuration

The entry point for configuring security, is to define a configuration class for our security needs, that extends from WebSecurityConfigurerAdapter and is annotated with the @Configuration and @EnableWebSecurity annotations.

These annotations provide us with lots of methods under the hood to enable security for a specific application context, that gets picked up while the application is booting up. Extending from the WebSecurityConfigurerAdapter class and overriding some of its methods will enable us to secure our http requests with custom features tailored to our needs, and, as typical in Spring, most of the wiring will happen during runtime under the hood. Let's see our base class:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private GreetingsBasicAuthenticationEntryPoint greetingsBasicAuthenticationEntryPoint;

    @Autowired
    private GreetingsUserDetailsService greetingsUserDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(greetingsUserDetailsService).passwordEncoder(passwordEncoder).getUserDetailsService();
    }

    public SecurityConfiguration() {
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/greet")
            .access("@userAuthorizationControl.checkAccessBasedOnRole(authentication)")
            .anyRequest()
            .authenticated().and().httpBasic()
            .authenticationEntryPoint(greetingsBasicAuthenticationEntryPoint);

        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

There's a lot going on here, so let's focus on the important code flows and details will follow later:

The body of the configure method is the most important part of the code: the HttpSecurity object allows for chaining of multiple other objects to construct a security filter chain that will be applied on the requests that follow the pattern in the antMatchers, which receives a URI with the endpoint name(s) we want to secure. Note that before we use the authorizeRequests() method to start narrowing down the access from an "authorize-all" policy.

The access method will be doing control access based on a certain user role. Spring has built-in user roles, some come with the default configurations, but, we can create and use application specific ones. Let's dive into our UserAuthorizationControl class:

@Service
public class UserAuthorizationControl {

    private static final String ADMIN = "GREET_ADMIN";

    public UserAuthorizationControl() {
    }

    public boolean checkAccessBasedOnRole(Authentication auth){
        if(auth.isAuthenticated() && !auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))) {
            return ((User)auth.getPrincipal()).getAuthorities().contains(new SimpleGrantedAuthority(ADMIN));
        }
        return false;
    }
}

We see that our class is a Spring service with a boolean method (as required by the access() chain call) that checks for two things: first if the user is authenticated (so if the user entered the correct credentials of username and password that the Authentication class expects) and if its authorities do NOT contain the "ROLE_ANONYMOUS" authority.

Authorities are a mechanism that Spring uses that grants the principal (simplifying, the user attempting to authenticate) specific roles within the context of the application that can be checked against for role-based access control.

Returning to our configure method, we see that after the role-based access check, we add the requirement for a user to be authenticated past that point as well as passing the httpBasic security filter.

Then we get to our basic authentication entry point that is usually required to send back a response to the user in case the authentication process fails. You need to define a realm (required by Spring) and you can choose which message to display, but all you need to know is that this will be triggered for cases when the authentication fails:

@Component
public class GreetingsBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {

    @Override
    public void commence(
        HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
        throws IOException {
        response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName());
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = response.getWriter();
        writer.println("HTTP Status 401 - " + authEx.getMessage());
    }

    @Override
    public void afterPropertiesSet() {
        setRealmName("greetRealm");
        super.afterPropertiesSet();
    }
}

We disable csrf() in the following line, because this would require additional tokens to be set and processed, and for most cases these things can be handled partially by ensuring same origin, checked with things like CORS, or generally protecting against network access at a higher level, so, it will be out of the scope of this explanation.

Finally, the creation policy ensures that Spring will never create an HttpSession and will never use it to obtain the current SecurityContext.

The AuthenticationManagerBuilder

The AuthenticationManagerBuilder is used to build an AuthenticationManager. This is an interface with a single method, authenticate that returns a fully authenticated object, including credentials. In our case, this implementation will be delegated to the WebSecurityConfigurerAdapter class.

In the method configureGlobal, we build our AuthenticationManager at a global level, to declare it will be used across the whole application for the URIs which are protected, and we do it by assigning it a password encoder (we use a standard one from Spring) so that the user passwords are stored encoded in the current securityContext. We also build it by setting a specific UserDetailsService implementation, which we will now look at in detail.

The UserDetailsService

This is the core interface which loads user-specific data and it is used throughout the framework as a user DAO and is the strategy used by the
DaoAuthenticationProvider that is a simple implementation that retrieves user details from a UserDetailsService.
This is the class where we will implement the logic to load the user details from any external source, and the ones which someone wanting to authenticate to our endpoint will use.
As stated earlier, these credentials can come from a database, we can use in-memory authentication, read them from the environment, etc. The most important thing here is to ensure that a new instance of UserDetails is returned instead of being simply retrieved and returned from the current source. This is because the underlying implementation, User in this case, is implementing an interface, CredentialsContainer, in order to allow the password to be erased after authentication. This may cause side-effects if we are storing instances in-memory and reusing them. To circumvent this, we can return a new copy of our UserDetails every time we invoke it. Our implementation looks like the following:

@Service
public class GreetingsUserDetailsService implements UserDetailsService, EnvironmentAware {

    private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Value("${username}")
    public String username;

    @Value("${password}")
    public String password;

    private UserDetails user;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        if (user.getUsername().equals(userName)) {
            return new User(user.getUsername(), user.getPassword(),
                Arrays.asList(new SimpleGrantedAuthority("GREET_ADMIN")));
        } else {
            throw new UsernameNotFoundException(userName);
        }
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.user = new User(username, passwordEncoder.encode(password), Collections.emptyList());

    }
}

We define our service to implement two interfaces that are important for our use-case:

  • UserDetailsService that makes us implement the loadUserByUsername method, that we need to use to retrieve the UserDetails that will be tied to the current principal, using our own custom logic.

  • EnvironmentAware which is an interface that provides us with the setEnvironment method, inside which the current environment is defined and is initialized. When auto-wiring the environment, some intermediate methods will be executed to set it, before the auto-wiring is valid, which effectively means that auto-wiring the environment won't work as intended when reading variables from it. See here for an explanation as to why this can happen.

Let's go over the core of this service step by step:

  • @Value("${username}") and @Value("${password}") are value annotations and they provide Spring with a way to load properties named by the values between the { } from the current environment. When running locally, these credentials can be stored in the application.properties file for example, which allows for running local tests using a tool like Postman (more on this later) or specifying test-only credentials to run integration tests specifically with the security component in place. When running in production, for example, even in an intranet, we might choose to load these credentials from a Docker environment. The great thing about it is that the logic and setup of the code can remain mostly unchanged, with the exception that you might need to provide dummy values for the user and password credentials so the JAR can be successfully generated, and then these default values will always be overwritten by the ones in the target environment. We specify default values with: @Value("${username:<some default value>}")

Then the method setEnvironment, uses the credentials read from the environment to create the new UserDetails with no Roles assigned to it:

this.user = new User(username, passwordEncoder.encode(password), Collections.emptyList());

the roles are only set after we can successfully load the user by its username, and return the new instance:

@Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        if (user.getUsername().equals(userName)) {
            return new User(user.getUsername(), user.getPassword(),
                Arrays.asList(new SimpleGrantedAuthority("GREET_ADMIN")));
        } else {
            throw new UsernameNotFoundException(userName);
        }
    }

Note that if we can not find the user for some reason, we throw the custom Spring exception UsernameNotFoundException that presents a useful error message.

It's also important to note here that the granted authority value we set here is the same as the one we are using in the UserAuthorizationControl filter shown earlier. In the event where several users can be loaded from an external source and have different values for their granted authorities, this check is what is binding the authorization to users that only have this specific authority.

Overview so far

Having walked through the security setup we have in place, we can do a quick recap:

  • Spring Security configuration is based on the WebSecurityConfigurer<T> interface, which the abstract class WebSecurityConfigurerAdapter implements, and which we extend from, to configure our own AuthenticationManagerBuilder, set our services for UserDetails, AuthenticationEntryPoint and also to define our HttpSecurity implementation, which defines which endpoints to secure, as well as the security types to use, like httpBasic() or token-based auth, etc. All of it lives in this main SecurityConfiguration class, which we need to annotate with @EnableWebSecurity.

  • The class UserAuthorizationControl is the one we use in the httpSecurity configuration to ensure that after a successful authentication the user has the allowed roles to access our endpoints.

Lastly, the GreetingsUserDetailsService is the service class that will be responsible for providing our own implementation for loading UserDetails implementations specific to our needs and created with our own logic.

Now, let's look at extending MockMvc to write integration tests that make use of externalized configuration to test our authentication with test data only.

Extending MockMvc to use test credentials to test the endpoint using authorization and authentication

Now that the security is setup in our Spring project, it's time for us to use the capabilities of MockMvc to write integration tests that allow us to test for the entire code flow of our endpoint including both authorization and authentication.

There are many ways to use MockMvc to write good integration tests, but, we will cover a practical one that can be deemed as "good enough" for our use-case.

What we will do is that we will write a basic test class, that initially is intended to work without having authentication in place, and then, from there, we will add exactly what we need to write a complete unit test. This isn't hard to illustrate here, for me, now, because while working on the code I was doing development in a very iterative and "fail-fast" way: write a class, write a test for it. Setup the endpoint, immediately test it in Postman. Then add authentication, it fails, improve the code, adapt the tests, etc, and during this feedback loop it's very easu to make fast progress. Let's look at the simple version of the integration tests:

@SpringBootTest
@AutoConfigureMockMvc
class GreetingsResourceTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void successIfValidRequest() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        PersonDTO personDTO = mapper.readValue("{\"name\":\"John\", \"age\":12}", PersonDTO.class);
        mockMvc.perform(MockMvcRequestBuilders.post("/greet")
            .contentType("application/json")
            .content(mapper.writeValueAsString(personDTO)))
            .andExpect(status().isOk());
    }

    @Test
    public void failsIfWrongContentType() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        PersonDTO personDTO = mapper.readValue("{\"name\":\"John\", \"age\":12}", PersonDTO.class);
        mockMvc.perform(MockMvcRequestBuilders.post("/greet")
            .contentType("text/plain")
            .content(mapper.writeValueAsString(personDTO)))
            .andExpect(status().isUnsupportedMediaType());
    }
}

MockMvc is a Spring class that allows to build a request to simulate a real request to the endpoint, while specifying things like the content-type, the requestBody, any authentication credentials and/or tokens and also assert on the expected status.

The above tests were written before the security configuration was in place, so they will now fail. Let's fix it.

Using the @TestPropertySource annotation to configure mock user credentials

Just like we do not want to define real user credentials in the application.properties file for a production case, we also do not want to use real credentials in the integration tests.

Spring provides us with a very useful annotation, the @TestPropertySource annotation, which allows us to configure environment variables just like they would be used if they were defined in a "physical" application.properties file. Here's how we can configure this :

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {"username = user1", "password = user1Pass"})
class GreetingsResourceTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void successIfValidRequest() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        PersonDTO personDTO = mapper.readValue("{\"name\":\"John\", \"age\":12}", PersonDTO.class);
        mockMvc.perform(MockMvcRequestBuilders.post("/greet")
            .with(SecurityMockMvcRequestPostProcessors.httpBasic("user1","user1Pass"))
            .contentType("application/json")
            .content(mapper.writeValueAsString(personDTO)))
            .andExpect(status().isOk());
    }

    @Test
    public void failsIfWrongContentType() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        PersonDTO personDTO = mapper.readValue("{\"name\":\"John\", \"age\":12}", PersonDTO.class);
        mockMvc.perform(MockMvcRequestBuilders.post("/greet")
            .with(SecurityMockMvcRequestPostProcessors.httpBasic("user1","user1Pass"))
            .contentType("text/plain")
            .content(mapper.writeValueAsString(personDTO)))
            .andExpect(status().isUnsupportedMediaType());
    }

    @Test
    public void failsIfNoCredentialsSupplied() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        PersonDTO personDTO = mapper.readValue("{\"name\":\"John\", \"age\":12}", PersonDTO.class);
        mockMvc.perform(MockMvcRequestBuilders.post("/greet")
            .contentType("application/json")
            .content(mapper.writeValueAsString(personDTO)))
            .andExpect(status().isUnauthorized());
    }
}

Essentially, what the annotation will do is, for the test context in Spring, the properties defined as sources for the tests will effectively replace the values that would be read from the application.properties file during runtime, and, since only the tests know about them, there's no risk that they will accidentally leak to production scenarios, so it's a very fast and easy way to configure user credentials for test usage only.

All the tests will know of this properties, and, the application context will pick them up as well, since it will know that we are running the tests, and the integration with our UserDetails implementation class will be able to be tested.

Using Postman and the application.properties to do a trial run

To finish the article, let's demonstrate the independency between test credentials and the application.properties file, by running a few tests in Postman.

Let's add these credentials in our application.properties file:

username=admin
password=password123

Postman allows you to test and develop APIs faster, in a very collaborative way and using an "API-first" design philosophy. It's very versatile and intuitive, so let's go and setup our first request in Postman:

configuration

As we can see, we can choose the type of the request, configure the URL and set the body of the request.

If we hit send, because we do not have any authentication configured, we will get the response:

unauthorized

Note how the message we see is exactly the message we had configured in our UnauthorizedEntryPoint class.

Finally, to showcase the happy path and confirm the independency between the test credentials and the ones defined in the application.properties, we can now setup Basic Auth in Postman and perform our request again:

success

And, we see that when setting up basic authentication while using Postman, the correct credentials are picked up from our application.properties file and we can ensure that the code functionally works with the mock credentials before deploying to production, just as we wanted!

Conclusion

This was a deep dive into the surface of what Spring Security can do. We saw how to setup security for our endpoint, how to support full integration tests, configure mock credentials for usage with MockMvc, we learned how to read properties from the environment, and we wrapped it up with a Postman test drive to show to how it all works!
Changing a bit from my previous posts, you can actually find a github repository here, with all the code: https://github.com/bruno-oliveira/spring-rest-api-features

Discussion

markdown guide