Code Review Videos > Java > First Steps with Spring Security & REST API

First Steps with Spring Security & REST API

Some readers of this blog may remember me from my time making and sharing Symfony (still the best PHP framework btw) training videos. I moved on from PHP generally a good few years back, but one of the things I miss most since moving primarily to JavaScript / TypeScript is the lack of a single, agreed upon full stack framework.

That’s why I have been dabbling more and more with Java’s Spring Boot.

And one of the biggest selling points about Spring Boot, and full stack frameworks generally, is their established security / authentication / authorisation approach. Symfony had the “security bundle”, and Spring Boot has the Spring Security framework.

But as the name “Spring Security framework” implies, it is in itself a beast of a thing. Quite daunting in fact, for a beginner.

In this post we are going to walk through one of, if not the most basic approaches to adding Login functionality to a Spring Boot rest / JSON API endpoint. It’s about getting hands-on experience, rather than just reading theory and not actually bothering to apply any of it.

Project Dependencies (Before Spring Security)

We’re going to start by adding our project dependencies. Since we’ve already covered how to set up a Spring Boot-based JSON API in previous posts, I won’t dwell on this section too much.

At this stage we won’t be adding in Spring Security.

This is because, by default, Spring Security will make our application secure. This means requiring security on all endpoints. That’s a good thing… but not when we are newbies who want some assurances our basic code is working in the first place.

I created this over on Spring Initializr:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>uk.co.a6software</groupId>
	<artifactId>spring-security-first-steps</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-security-first-steps</name>
	<description>Demo project for Spring Boot with Spring Security and Basic auth in a JSON API</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>Code language: HTML, XML (xml)

Creating The API Endpoints

Next, we’ll create two API endpoints.

One will be publicly accessible, available to everyone whether they provide a username and password or not.

The other will be restricted, only accessible if valid credentials are provided.

The implementation of these endpoints is completely independent of the security configuration we will shortly add.

As the creation of these types of endpoints is something we’ve covered in previous posts, we won’t dwell too long here:

// /src/main/java/uk/co/a6software/spring_security_first_steps/controllers/SomeEndpointsController.java

package uk.co.a6software.spring_security_first_steps.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SomeEndpointsController {

    @GetMapping("/open-to-all")
    public String openToAll() {
        return "Welcome one and all!";
    }

    @GetMapping("/restricted-to-authenticated-users")
    public String hello() {
        return "Hello, authenticated user!";
    }
}Code language: Java (java)

This should be enough to get our app booting, and be able to hit both endpoints.

From the Controller code, click on the little green circle next to any controller action to generate a request for testing that action:

That should pop up something like the following:

Excellent.

But rather than relying on us manually sending requests in to test our API, we should probably automate that process, right?

Right.

Fortunately, we learned how to do that last time.

Creating the test file is also nice and easy in IntelliJ, thanks to a little shortcut I found out about. Simply put your cursor over the file name and press option + return on the Mac (not sure on other platforms, I’m writing this on a Mac so…) which gives a little pop-up:

And then when you click ‘Create test’, it gives another prompt. I just clicked ‘OK’:

Sweet.

Now we have an empty test file which we can update to the following:

// /src/test/java/uk/co/a6software/spring_security_first_steps/controllers/SomeEndpointsControllerTest.java

package uk.co.a6software.spring_security_first_steps.controllers;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(SomeEndpointsController.class)
class SomeEndpointsControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void testOpenToAll() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/open-to-all"))
                .andExpect(MockMvcResultMatchers.content().string("Welcome one and all!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    public void testRestrictedToAuthenticatedUsers() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/restricted-to-authenticated-users"))
                .andExpect(MockMvcResultMatchers.content().string("Hello, authenticated user!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}Code language: Java (java)

Again, there is nothing new here that we haven’t seen / learned already.

What I will say is that I guessed at most of this, and with help from IntelliJ and a bit of my memory, I got it mostly right. So there is something to be said for building muscle memory, and judicious use of autocomplete. For the rest I had to refer back to my previous post and GitHub archive.

These tests should both pass.

But as soon as we add in Spring Security, both are going to start failing.

Adding In The Spring Security Dependencies

We have a working project!

Let’s break it!

Update the pom.xml with the new dependencies:

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

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>Code language: HTML, XML (xml)

If it’s not obvious, I have omitted everything else for brevity.

Pro Tip: Whenever you update your pom.xml file in IntelliJ, be sure to click the little (some might say tiny) blue elephant icon. If you don’t do this, you won’t actually have the dependencies installed:

As soon as you do this, and you re-run your tests… well, now they are failing:

One thing you will see if you start your implementation now is the automatic creation of a ‘generated user’:

This is good. It’s confirmation our app is working, and that Spring Security is doing … stuff.

Manually Testing An Authenticated Request

It would be really nice to be able to send in an automated test that adds the username and password, and can reliably and repeatedly tell us our endpoints are secure where expected.

But let’s walk before we can run.

To start, we will make use of those generated requests we had earlier.

However, when we send those requests now they are, as expected, returning a 401 because they are secured / restricted by default.

What we are going to do is update those requests to become authorised.

In the top right of the IDE when you have the requests tab open, you should be able to see ‘Examples’.

Click that and one such example is ‘Requests with Authorisation’ – spelled the American way, because Jetbrains are confused as to the correct and proper spelling.

This will show an example. It looks something like this:

### Basic authorization.
GET https://examples.http-client.intellij.net/basic-auth/user/passwd
Authorization: Basic user passwdCode language: HTTP (http)

So we need to update the username and password.

Well, we don’t need to update the username.

user is the default generated username.

The password, however, will need to be retrieved. If it’s not open right now, you can find it on by clicking the ‘Run’ button in the bottom left. Or just re-running the project.

Note, it changes every time you restart the application.

If you want to customise one, or either, then update the src/main/resources/application.properties file:

spring.security.user.name=billy
spring.security.user.password=your_password_here

Then update the Authorization header info appropriately:

Your request should now succeed:

(Naive) Automated Testing An Authenticated Request

Somehow we need to do this exact same process but in our tests.

It sounds really hard, but as this is obviously a very common use case, there is a really simple solution.

We need to annotate our tests with @WithMockUser. And that’s it:

package uk.co.a6software.spring_security_first_steps.controllers;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(SomeEndpointsController.class)
class SomeEndpointsControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void testOpenToAll() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/open-to-all"))
                .andExpect(MockMvcResultMatchers.content().string("Welcome one and all!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    @WithMockUser
    public void testRestrictedToAuthenticatedUsers() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/restricted-to-authenticated-users"))
                .andExpect(MockMvcResultMatchers.content().string("Hello, authenticated user!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}
Code language: Java (java)

With those two changes (the import and the use of the annotation), if we run the tests we now see one pass, one fail:

Remember, the endpoints are restricted by default.

At present, both need an authenticated user.

It’s worth noting that the @WithMockUser annotation uses a default username of user, whatever random password is generated magically in the background, and would have the role of ROLE_USER.

Note that when I ran the tests above, I had manually configured my username in the application.properties file as covered above, so these two things are independent of one another.

Before we move on, let’s configure all four test scenarios:

package uk.co.a6software.spring_security_first_steps.controllers;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(SomeEndpointsController.class)
class SomeEndpointsControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void testOpenToAllWhenUnauthenticatedIsSuccessful() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/open-to-all"))
                .andExpect(MockMvcResultMatchers.content().string("Welcome one and all!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    @WithMockUser
    public void testOpenToAllWhenAuthenticatedIsSuccessful() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/open-to-all"))
                .andExpect(MockMvcResultMatchers.content().string("Welcome one and all!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    public void testRestrictedToAuthenticatedUsersIsInaccessibleWhenLoggedOut() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/restricted-to-authenticated-users"))
                .andExpect(MockMvcResultMatchers.status().is(401));
    }

    @Test
    @WithMockUser
    public void testRestrictedToAuthenticatedUsersIsAccessibleWhenLoggedIn() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/restricted-to-authenticated-users"))
                .andExpect(MockMvcResultMatchers.content().string("Hello, authenticated user!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}Code language: JavaScript (javascript)

Note: these tests will not work as expected. If copy / pasting, know that these tests will not pass without the further changes below!

This means:

  1. Any user, logged in or not, can access /open-to-all
  2. Only logged in users can see /restricted-to-authenticated-users

With the default config, three of the four tests pass here:

Anyway, let’s actually configure the application to make all tests pass.

Adding Spring Security Configuration

The last thing we will do today is add in the Spring Security Configuration to make our app behave as expected.

The gist is:

  1. We want to allow access to authenticated users to explicitly stated routes.
  2. We want to allow access to authenticated users to publicly accessible routes.
  3. We want to allow access to unauthenticated users to explicitly stated routes.
  4. Deny everything else.

And here’s how we do that:

// /src/main/java/uk/co/a6software/spring_security_first_steps/config/SecurityConfig.java

package uk.co.a6software.spring_security_first_steps.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/open-to-all").permitAll()
                        .anyRequest().authenticated()
                )
                .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}Code language: Java (java)

It seems like the syntax for defining the SecurityFilterChain has changed significantly in the major version bumps of Spring Security. I see that Spring Security v7.0 is due … soon? This config, using lambdas, is the preferable way forward.

Anyway, the config above should be enough to get your app up and running.

The tests, however, will not yet pass.

Beware Method Naming Madness

I hit on a really curious issue when allowing IntelliJ to help auto-complete this class.

By default it suggested the method name of springSecurityFilterChain, giving the code:

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain springSecurityFilterChain(HttpSecurity http) throws Exception {
        http
Code language: Java (java)

When starting the application directly, I got the following error:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method springSecurityFilterChain in uk.co.a6software.spring_security_first_steps.config.SecurityConfig required a bean of type 'org.springframework.security.config.annotation.web.builders.HttpSecurity' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.security.config.annotation.web.builders.HttpSecurity' in your configuration.Code language: JavaScript (javascript)

Not at all obvious what that meant, it took me a bit of Googling to find some advice to rename the method.

I changed from springSecurityFilterChain to filterChain and the app loaded.

Fixing The Automated Tests

I’ve banged on at various points in this post about the tests being incorrect.

This one caught me out, I won’t deny it.

Let’s see the code, then walk it through:

package uk.co.a6software.spring_security_first_steps.controllers;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@SpringBootTest
@AutoConfigureMockMvc
class SomeEndpointsControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testOpenToAllWhenUnauthenticatedIsSuccessful() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/open-to-all"))
                .andExpect(MockMvcResultMatchers.content().string("Welcome one and all!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    @WithMockUser
    public void testOpenToAllWhenAuthenticatedIsSuccessful() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/open-to-all"))
                .andExpect(MockMvcResultMatchers.content().string("Welcome one and all!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    public void testRestrictedToAuthenticatedUsersIsInaccessibleWhenLoggedOut() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/restricted-to-authenticated-users"))
                .andExpect(MockMvcResultMatchers.status().is(401));
    }

    @Test
    @WithMockUser
    public void testRestrictedToAuthenticatedUsersIsAccessibleWhenLoggedIn() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/restricted-to-authenticated-users"))
                .andExpect(MockMvcResultMatchers.content().string("Hello, authenticated user!"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}
Code language: Java (java)

We cannot use the @WebMvcTest annotation for these tests.

Yes, we can mock users (@WithMockUser), and yes, the tests do behave somewhat as we expect. But once Spring Security rules need testing, as best I can discern, we really do need the entire Spring application context.

And the only way we can get that is to change to use the @SpringBootTest annotation.

This then allows the application to behave as it would in the real environment. This includes full configuration of beans, security, services, repositories, etc. The downside? It’s slower to run those tests, as there is more to ‘spin up’.

If we use the @SpringBootTest annotation then in order for the tests to properly set up the MockMvc instance then we also need the @AutoConfigureMockMvc annotation, too.

And that’s really it for this post. For such a ‘basic’ task there was quite a lot to cover. Though from what I’ve read and seen in docs and elsewhere, this is barely (and I do mean barely) scratching the surface.

Example Code

You can find the code for this post over on GitHub.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.