Setting up Spring security

Let us start with a very simple application. Using your IDE, create a Spring boot application with the following dependencies: Web, JPA and H2 (let us just keep it simple). To start with our experience, let us add the very first controller we used in our course:

@RestController
public class GreetingsController {
    @GetMapping("/sayHello")
    public String sayHello() {
        return "Hello world";
    }
}

That is it, you can go to your browser an test the application (e.g. go to the web page http://localhost:8080/sayHello).

Now, add the following maven dependency to your project’s pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

If you restart your application and go again to http://localhost:8080/sayHello you will notice that you are not longer authorized. In fact, the application will redirect you to a login page, where you are expected to enter your credentials.

If you check the application logs, you will see that it printed out a message similar to the one below:

Using generated security password: 478c81b2-257e-4c2c-84f4-fb6c39555ff0

When Spring boot runtime detects the jars associated with spring-boot-starter-security, it will change the application to add a basic infrastructure to secure the access to the application’s web pages. No additional changes would be required if we agree with the default settings. Unfortunately, the default configuration is such that we have one single user user with a randomly generated password, e.g. the one that is dumped into the logs. Try out again the application, now that you know the credentials. Definitively, we would need to change the default configuration.

In fact, you can set the password for the default user on the application.properties file. For instance, you can add the following into such file:

spring.security.user.password=password

and, when you restart your application, you will be able to use password for logging-in. You will also notice that the above overrides the configuration such that no random password is generated anymore.

However, the approach is not robust and is something you would not certainly use in production. Therefore, I will ask you first to remove the line setting up the password inside the application.properties file. Then, we will learn how to setup a more appropriate authentication service with Spring security.

Adding in memory authentication

Firstly, we will setup an authentication service that keeps all the information for authentication in main memory. To this end, add a configuration class (in a separate file, if you wish) with the following code.

@Configuration
public class SecurityConfigurator {
    @Autowired
    void authentication(AuthenticationManagerBuilder auth) throws Exception {
        UserBuilder userBuilder = User.withDefaultPasswordEncoder();
        auth.inMemoryAuthentication()
                .withUser(userBuilder.username("user1").password("user1").roles("USER1"))
                .withUser(userBuilder.username("user2").password("user2").roles("USER2"));
    }
}

I believe that the code above is self-descriptive but, just in case, it registers two users into the authentication service. You will notice that the method withDefaultPasswordEncoded is marked as deprecated. Do not be worried about that, it will work anyway. In fact, we are using such default encoder because we are hard coding the actual password and expecting that such passwords are encoded by the default encoder. Hard coding the passwords is a bad practice, and that is the reason why the method withDefaultPasswordEncoded is reported as deprecated.

Configuring access rules

There are several ways to configure authorization and we will start with the one referred to as "security rules". The idea with this approach is to specify global rules, using the paths on URLs to filter the access of users. This is also accomplished with a configuration class. Although we can use separate configuration classes, I will propose to use a single class. In fact, our application is simple such that the corresponding configuration will remain short.

To avoid confusion, replace fully the code of the configuration class that we used in the previous section with the following code:

@Configuration
public class SecurityConfigurator extends WebSecurityConfigurerAdapter {
    @Autowired
    void authentication(AuthenticationManagerBuilder auth) throws Exception {
        UserBuilder userBuilder = User.withDefaultPasswordEncoder();
        auth.inMemoryAuthentication()
                .withUser(userBuilder.username("user1").password("user1").roles("USER1"))
                .withUser(userBuilder.username("user2").password("user2").roles("USER2"));
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/sayHello").hasAnyRole("USER1")
                .antMatchers("/api/**").authenticated()
            .and()
            .formLogin().permitAll();
    }
}

The changes are subtle but worth discussing. First, we made class SecurityConfigurator to extend WebSecurityConfigurerAdapter. The latter class is the one that concentrates all the security default settings and we will change them by overriding the method configure. This method receives as parameter a reference to HttpSecurity. If you analyze carefully the code of that method, you will identify three parts:

  • In the first part, we are disabling the protection to cross-site request forgery attack (abbreviated as CSRF). The latter attach usually relies on hijacking cookies to simulate authorized accesses. However, in our context we would be working with REST interactions and, hence, we would not be using cookies such that CSRF does not make sense (its protection relies on the use of tokens over HTTP headers, such that it is better to disable CSRF to avoid handling the tokens).

  • In the second part, we define global access rules, which I am illustrating with three examples:

    • We are authorizing the access to the home page (i.e. "/") to any user, authenticated or not,

    • We are restricting the access to the page "/sayHello" to users having the role "USER1", and

    • We are requiring that any access to paths matching the expression "/api/**" is granted only to authenticated users.

  • In the last part, we are opening the access to the login form to any user. Note, however, that this last part is there only for demonstration purposes: providing a login form does not make sense if we are accessing the functionality via REST interactions.

Restart the application a play with it using the browser.

Setting up HTTP Basic

Well, we are interested in adding securing the access to our REST APIs. Therefore, it does not sense to have a login page and we have to find a way to communicate the security tokens over REST interactions. In that respect, we can find several possible approaches to this problem including JSON web tokens (JWS) and others. To keep it simple, I decided to use a simpler approach, i.e. HTTP Basic, because we would be using several types of technologies and using JWS would require lot of configuration for each one of them.

Well, let us first update the security configuration class. To that end, replace the method configure on class SecurityConfigurator with the following code:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .authorizeRequests(
                .antMatchers("/sayHello").hasAnyRole("USER1")
                .antMatchers("/api/**").authenticated()
            .and().httpBasic();
}

As you can see, we have removed most of the lines that were intended to facilitate the demonstration and added a new line with a call to httpBasic(). The latter is the one that adds a spring component (a filter), that intercepts the REST interactions and tries to get the authentication token from the HTTP headers.

In the client side, the HTTP basic access authentication protocol implies the following:

  1. The username and password are concatenated with a colon in between them to generate a single string, preferably using UTF-8,

  2. The resulting string in the previous step is then encoded using a variant of Base64, and

  3. The encoded string is included in every HTTP request as the authorization header as: Authorization: Basic dXNlcjE6dXNlcjE=, where dXNlcjE6dXNlcjE= is the encoded security token.

To test the application, I propose you to use Postman. Configure a GET interaction over http://localhost:8080/sayHello . You will notice that Postman provide a tab "Authentication" where, after selecting Basic Auth, entering "user1" both as username and password, and selecting Update Request, you will see that a Header will be added with the corresponding information. So, let us give a try. If everything is configured as specified here, you should be able to see "Hello world" in the response body shown by Postman. If something is wrong check that you restarted the application and entered the correct credentials. BTW, it would interesting to try with the other user or with wrong credentials to see the result.

Method level authorization

We will now introduce another approach to specifying the authorization, where the control happens at the level of the method. It is worth mentioning that this approach is very similar to the one used by Java enterprise edition (JEE), so you might come across with a piece of code that looks similar to ours.

Let us start by adding the annotation @EnableGlobalMethodSecurity(securedEnabled=true) to our SecurityConfigurator class. In fact, you can add it to any class that carries the annotation @Configuration or to the project’s entry class.

As way of example, copy the following code there:

@RestController
@RequestMapping("/api/services")
public class SecuredController {
    @GetMapping("/resource1")
    @Secured({"ROLE_ADMIN", "ROLE_USER1"})
    public String getResource1() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        System.out.println("Hello " + username);
        return "{\"msg\": \"You've reached resource1\"}";
    }

    @GetMapping("/resource2")
    @Secured({"ROLE_ADMIN", "ROLE_USER2"})
    public String getResource2() {
        return "{\"msg\": \"You've reached resource2\"}";
    }
}

On the code above, we are defining two REST endpoints and we are assuming that the application recognizes three possible roles: "ADMIN", "USER1" and "USER2". Now, the annotation @Secured allows you to specify the roles to which you grant permission to access the underlying method. Note that the roles are specified with the prefix ROLE_. However, role ROLE_USER1 corresponds with USER1 in the configuration, so on and so forth. With this information you would straightforwardly understand that getResource1() will only be accessible to user user1 and that getResource2() will only be accessible to user user2, according to our current configuration.

Please also notice that in the first method, I intentionally added one line that allows us to get access to the user information from the SecurityContextHolder. This approach could be used to determine who is the issuer of a request (e.g. the name of the site engineer that is creating the plant hire request).

Configuring Cross-Origin Request Sharing (CORS)

Since our REST API is to be access from a Javascript frontend application, we would need to configure CORS. In fact, we already know that we can use the annotation @CrossOrigin on every single rest controller we want to make available to the frontend application. However, we also need to adapt the configuration of Spring security, as shown below.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().and().cors()
            .authorizeRequests(
                .antMatchers("/sayHello").hasAnyRole("USER1")
                .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll()
                .antMatchers("/api/**").authenticated()
            .and().httpBasic();
}

As you can see, the changes are simple but deserve a few words of explanation. First, the calls .and().cors() add the components to support the mechanism (also in the form of a filter). On the other hand, the sequence of calls .antMatchers(HttpMethod.OPTIONS, "/api/").permitAll() is intended to allow the interactions with the backend to complete interactions required by CORS (the so-called preflight** which relies on a request with HTTP OPTIONS).

Configuring a JDBC-based authentication service

Adding a JDBC-based authentication service to our application is easy with Spring security. In fact, we can use the same database that we use for storing the application domain classes even if the latter are accessed with JPA repositories. To this end, we need basically two things. First, we need to get access to the JDBC data source, which can be done with an autowired annotation as shown in the code below. Second, we need to configure the authentication service. The latter can be done with the sequence of calls: .jdbcAuthentication().dataSource(dataSource).withDefaultSchema(). Note that we are passing the reference to the data source that was injected by the runtime and that we are asking the runtime to use the default database schema. That means, we are allowing spring runtime to setup the authentication database and, as we are using the embedded H2, it will be reset every time we restart the application. Of course, you can configure this approach to use a different database and, why not, to preserve the database content.

@Configuration
public class SecurityConfigurator {
    @Autowired
    private DataSource dataSource;

    @Autowired
    void authentication(AuthenticationManagerBuilder auth) throws Exception {
        UserBuilder userBuilder = User.withDefaultPasswordEncoder();
        auth.jdbcAuthentication().dataSource(dataSource).withDefaultSchema()
                .withUser(userBuilder.username("user1").password("user1").roles("ADMIN", "USER1"))
            .and()
            .inMemoryAuthentication()
                .withUser(userBuilder.username("user2").password("user2").roles("USER2"));
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().and().cors()
                .authorizeRequests(
                    .antMatchers("/h2-console/**").permitAll()
                    .antMatchers("/sayHello").hasAnyRole("USER1")
                    .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll()
                    .antMatchers("/api/**").authenticated()
                .and().httpBasic();
        http.headers().frameOptions().disable();
    }
}

Please note that I added a couple of configuration lines to open the access to H2’s console, i.e. .antMatchers("/h2-console/**").permitAll() and http.headers().frameOptions().disable();. You can safely remove them from the configuration class if you are not interested in getting access to H2’s console.