Securing a ReST interface with SpringBoot using Basic Authentication

In this article I aim to demystify the experience of securing a ReST API with SpringBoot using the HTTP Basic Authentication mechanism. Source code for this project is available on GitHub.

This project will be a ground-up, complete example comprising of a secured API endpoint and a publicly accessible endpoint. The sometimes messy internals of Spring Security will be explored in detail, including gotchas that I’ve found along the way.

The user information (username and password) is passed to the API via Basic Authentication. In a web browser this will be an internal popup requesting the information. In a tool such as Postman, this information will be entered under the Authorization tab, with the selection of “Basic Auth”.

The data store for authenticating users on the backend shall be a Postgres database with a simple table. Initially however, I will show how to authenticate users using a hardcoded in-memory user. This solution can be chosen if you do not want to use a database – though it doesn’t mimic a real world scenario very well!

Firstly, set up a SpringBoot application with the following dependencies

<dependencies>

       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-jpa</artifactId>
       </dependency>
       <dependency>
           <groupId>org.postgresql</groupId>
           <artifactId>postgresql</artifactId>
           <scope>runtime</scope>
       </dependency>
   </dependencies>

Configure a Postgres database:

  • Create a schema called micros
  • Create a table called users

The following DDL can be used to create the tables

create table micro.users
(
id serial
constraint users_pk
primary key,
username varchar not null,
password varchar not null,
last_login timestamp,
roles varchar
);

Use a database tool to insert data in to this table. For now, we require a value for username, password and role. Insert values similar to:

INSERT INTO micro.users (id, username, password, last_login, roles) VALUES (1, 'rohan', 'password', null, 'ADMIN');

 

For our secured API, we are therefore assuming that user rohan will be able to access any API that has role security of ADMIN. More on that later.

Set up application.properties in SpringBoot to connect to our Database

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.show-sql=true
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=rocknroll

With the housekeeping out of the way, lets turn to the SpringBoot application and create a very simple API for a client to use.

Rest Controller

@RestController
@RequestMapping("/api")
public class HelloService {

    @GetMapping(path = "/hello")
    @PreAuthorize("hasRole('ADMIN')")
    public HelloObject sayHello() {
        HelloObject helloObject = new HelloObject();
        helloObject.setData(new SimpleDateFormat("yyyyMMdd HHmmss").format(new Date()));
        helloObject.setNumber(1);
        return helloObject;
    }

    @GetMapping(path = "/goodbye")
    public HelloObject sayGoodBye() {
        HelloObject helloObject = new HelloObject();
        helloObject.setData(new SimpleDateFormat("yyyyMMdd HHmmss").format(new Date()));
        helloObject.setNumber(2);
        return helloObject;
    }

}

Our return object is a simple JSON

public class HelloObject {

    private String data;
    private Integer number;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }
}

At this point we want to test our unsecured API works correctly, so we will remove the PreAuthorize anotation. The API should serve back a JSON response when tested with the following URL http://localhost:8080/api/hello

(If there is a 401 response, you will need to remove springboot-starter-security from the pom.xml)

To secure the application, add the following dependency to the POM file

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

Note, now that we have added this dependency, SpringBoot will automatically fail all responses. This is normal, we need to set up Spring Security to enable this API.  By default all traffic will be rejected unless specifically configured otherwise. And from a security standpoint this is a good thing!

Next we will configure Spring Security. This will be in a file called SecurityConfig. It will extend WebSecurityConfigurerAdapter. Here we will specify that our endpoint shall be protected. In addition we will allow another endpoint to be publicly accessible.

Web security configuration

@Configuration
@EnableWebSecurity
//The @EnableWebSecurity is a marker annotation. It allows Spring to find (it's a @Configuration and, therefore, @Component) and automatically apply the class to the global WebSecurity.
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private BCryptPasswordEncoder encodedPass;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
        //auth.inMemoryAuthentication().withUser("rohan").password("$2a$10$a89v06ied5ZWvnL5raVilOymiLGOrWs1LeDC/vVoYaz4tlj7Bmd16").roles("ADMIN");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().and().authorizeRequests()
                .antMatchers("/api/goodbye").permitAll()
                .antMatchers("/api/**").hasRole("ADMIN")
                .and().csrf().disable()
    }
}

The directive (really just a method call) httpBasic enables httpBasic authentication. We need this to field the username and password being passed in. authorizeRequests() will do as it implies – authorize all requests that are made and return an error  (401) otherwise . antMatchers() applies logic to paths specified. In this case anything under /api/ should return successfully only for a user with ADMIN role.

We placed the permitAll() for our public API above the general condition as the /** will capture everything, including ‘goodbye’. So if placed underneath it won’t work.
Since we are not using a browser we don’t need cross-site-request-forgery checking so we disable it.

Method level security

Make a small configuration class to enable method level annotations for @PreAuthorize

We can also add our BCrypt password encoder bean here. I tried adding this in SecurityConfig class but got a circular reference error preventing the application from starting, so to fix that I put it here.

@Configuration
@EnableGlobalMethodSecurity(
        prePostEnabled = true,
        securedEnabled = true,
        jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Bean
    public BCryptPasswordEncoder encodedPass(){
        return new BCryptPasswordEncoder();
    }

}

Now lets set up the User Service

Note: If you want to check only against an in user memory registry, you can do this by commenting out the UserService line and uncommenting auth.inMemoryAuthentication

@Component
public class CustomUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {

    @Autowired
    private UsersDao usersDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Users user = usersDao.findByUsername(username);
        String[] role = user.getRoles().split(",");
        List<GrantedAuthority> authorities = new ArrayList<>();
        if (role != null && role.length > 0) {
            for (String aRole: role) {
                authorities.add(new SimpleGrantedAuthority("ROLE_" + aRole.toUpperCase()));
            }
        }
        // see https://stackoverflow.com/questions/37615034/spring-security-spring-boot-how-to-set-roles-for-users
        //UserDetails userdeets = new User(user.getUsername(),  "$2a$10$deUsrZi0ZtpbBhwov0ODDOxbsgMGoRhv/c7f5yLEAr2cQ18itHG8q", authorities);
        UserDetails userdeets = new User(user.getUsername(),  new BCryptPasswordEncoder().encode(user.getPassword()), authorities);
        System.out.println("UserDetails: "+user.getUsername());
        return userdeets;
    }
}

 

The CustomUserService will extend Springs UserService. It’s task is to take input from Basic Authentication and use this to authenticate the user. In our case we are using it as a look up to a database table. We add all the comma separated roles that are entered in the ‘roles’ field in our database table. For now we should just have ADMIN in this roles database field.

A couple of gotchas: In order to authenticate a role as specified in the @PreAuthorize annotation, we need to add the string ROLE_ to the role name.

We also must encode our password on our side when attempting to match the password. SpringBoot will automatically present the passed in value retrieved via Http Basic Auth as BCrypt encoded. It occurs behind-the-scenes when we add the .httpBasic directive. You can test this for yourself!

Testing the application

If we add username and password to Basic Authentication fields and make a call to our protected /hello API, we should get a response such as this:

{
    "data": "20220309 172800",
    "number": 1
}

If we make the username or password wrong, we should get a response like this:

{
    "timestamp": "2022-03-09T06:30:09.613+00:00",
    "status": 401,
    "error": "Unauthorized",
    "path": "/api/hello"
}

There is a slight hiccup that I found. If we successfully authenticate and then change the password, it will still return a valid response – why? SpringBoot session management is caching the credentials .  To overcome this, add the following line to the Spring Security configuration (after crsf disable)

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

This will now check the password is OK on every invocation which is what we want.

Full source code is available at: https://github.com/rptrus/MicroServiceA

 

Leave a Reply

Your email address will not be published. Required fields are marked *