6. Security Basic Authentication - Spring Boot vs Micronaut Framework - Building a Rest API

This guide compares how to secure a REST API with basic authentication in Micronaut and Spring Boot applications.

Authors: Sergio del Amo

Micronaut Version: 4.6.3

1. Sample Project

You can download a sample application with the code examples in this article.

2. Introduction

This guide is the sixth tutorial of Building a Rest API - a series of tutorials comparing how to develop a REST API with Micronaut Framework and Spring Boot.

In this tutorial, we secure the API via basic authentication. Only users with the role SAAS_SUBSCRIPTION_OWNER can access the endpoints. Each subscription is associated with a user. Authenticated users can only access their own subscriptions.

3. Dependencies

3.1. Spring Boot

In the Spring boot application, add Spring Boot Starter Security dependency:

build.gradle
implementation("org.springframework.boot:spring-boot-starter-security")

3.2. Micronaut Security

In the Micronaut application, add Micronaut Security and Spring Security Crypto. We use the latter to encrypt the user’s password using BCrypt.

build.gradle
implementation("io.micronaut.security:micronaut-security")
implementation("org.springframework.security:spring-security-crypto:6.2.0")

4. Entity

We add an owner property to the SaasSubscription entity. The owner is the unique identity of user person who created and can manage a subscription.

4.1. Spring Boot

springboot/src/main/java/example/micronaut/SaasSubscription.java
package example.micronaut;

import org.springframework.data.annotation.Id;

record SaasSubscription(@Id Long id, String name, Integer cents, String owner) {
}

4.2. Micronaut Framework

micronautframework/src/main/java/example/micronaut/SaasSubscription.java
package example.micronaut;

import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;

@Serdeable (1)
@MappedEntity (2)
record SaasSubscription(@Id  (3)
                        @GeneratedValue (4)
                        Long id,
                        String name,
                        Integer cents,
                        String owner) {
}
1 Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized.
2 Annotate the class with @MappedEntity to map the class to the table defined in the schema.
3 Specifies the ID of an entity
4 Specifies that the property value is generated by the database and not included in inserts

5. Repository

We need to modify the repositories to add methods leveraging the owner property.

5.1. Spring Boot

springboot/src/main/java/example/micronaut/SaasSubscriptionRepository.java
package example.micronaut;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;

interface SaasSubscriptionRepository extends CrudRepository<SaasSubscription, Long>, (1)
        PagingAndSortingRepository<SaasSubscription, Long> { (2)

    SaasSubscription findByIdAndOwner(Long id, String owner);

    Page<SaasSubscription> findByOwner(String owner, PageRequest pageRequest);
}
1 By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations.
2 Extend PagingAndSortingRepository to support paging and sorting.

5.2. Micronaut Framework

micronautframework/src/main/java/example/micronaut/SaasSubscriptionRepository.java
package example.micronaut;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.PageableRepository;

import java.util.Optional;

@JdbcRepository(dialect = Dialect.H2) (1)
interface SaasSubscriptionRepository extends PageableRepository<SaasSubscription, Long> { (2)

    Optional<SaasSubscription> findByIdAndOwner(Long id, String owner);

    Page<SaasSubscription> findByOwner(String owner, Pageable pageRequest);
}
1 @JdbcRepository with a specific dialect.
2 PageableRepository extends CrudRepository, which provides automatic generation of CRUD (Create, Read, Update, Delete) operations and adds methods for pagination.

6. Security Configuration

Two sample users will be able to authenticate: sarah1 and john-owns-no-subscriptions. Only the former has the role SAAS_SUBSCRIPTION_OWNER.

6.1. Spring Boot

The following class configures Spring Security for the application:

springboot/src/main/java/example/micronaut/SecurityConfig.java
package example.micronaut;

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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration (1)
class SecurityConfig {

    private static final String ROLE_SAAS_SUBSCRIPTION_OWNER = "SAAS_SUBSCRIPTION_OWNER";
    private static final String ROLE_NON_OWNER = "NON-OWNER";

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

    @Bean (3)
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(r -> r.requestMatchers("/subscriptions/**") (4)
                        .hasRole(ROLE_SAAS_SUBSCRIPTION_OWNER))
                .httpBasic(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .build();
    }

    @Bean (5)
    UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) {
        User.UserBuilder users = User.builder();
        UserDetails sarah = users
                .username("sarah1")
                .password(passwordEncoder.encode("abc123"))
                .roles(ROLE_SAAS_SUBSCRIPTION_OWNER)
                .build();
        UserDetails hankOwnsNoCards = users
                .username("john-owns-no-subscriptions")
                .password(passwordEncoder.encode("qrs456"))
                .roles(ROLE_NON_OWNER)
                .build();
        return new InMemoryUserDetailsManager(sarah, hankOwnsNoCards); (6)
    }
}
1 The @Configuration annotation tells Spring to use this class to configure Spring and Spring Boot itself. Any Beans specified in this class are available to Spring’s Auto Configuration engine.
2 Create a bean of type PasswordEncoder
3 Create a bean of type SecurityFilterChain to configure Spring Security’s filter chain.
4 All HTTP requests to /subscriptions/** endpoints are required to be authenticated using HTTP Basic Authentication security (username and password). They do not require CSRF security.
5 Create a bean of type UserDetails.
6 InMemoryUserDetailsManager is a test-only Service that Spring security provides as a way to provide user authentication and authorization information.

6.2. Micronaut Framework

In Micronaut Security, Basic authentication is enabled by default. Micronaut Security attempts to authenticate the supplied credentials against every bean of type AuthenticationProvider. Write the following authentication provider singleton which authenticates the same sample users as we did in the Spring Boot application.

micronautframework/src/main/java/example/micronaut/AppAuthenticationProvider.java
package example.micronaut;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.provider.HttpRequestAuthenticationProvider;
import jakarta.inject.Singleton;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;

@Singleton (1)
class AppAuthenticationProvider<B> implements HttpRequestAuthenticationProvider<B> {

    private static final String KEY_PASSWORD = "password";
    private final Map<String, Authentication> users;
    private final PasswordEncoder passwordEncoder;

    AppAuthenticationProvider() {
        passwordEncoder = new BCryptPasswordEncoder();
        users = Map.of("sarah1",
                Authentication.build("sarah1",
                        Collections.singletonList("SAAS_SUBSCRIPTION_OWNER"),
                        Collections.singletonMap(KEY_PASSWORD, passwordEncoder.encode("abc123"))),
                "john-owns-no-subscriptions",
                Authentication.build("john-owns-no-subscriptions",
                        Collections.singletonList("NON-OWNER"),
                        Collections.singletonMap(KEY_PASSWORD, passwordEncoder.encode("qrs456"))));
    }

    @Override
    public @NonNull AuthenticationResponse authenticate(@Nullable HttpRequest<B> request,
                                                        @NonNull AuthenticationRequest<String, String> form) {
        return Optional.ofNullable(users.get(form.getIdentity()))
                .filter(a -> passwordEncoder.matches(form.getSecret(), a.getAttributes().get(KEY_PASSWORD).toString()))
                .map(authentication -> AuthenticationResponse.success(form.getIdentity(), authentication.getRoles()))
                .orElse(AuthenticationResponse.failure());
    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.

7. Controllers

Both frameworks allow you to bind the authenticated user to a controller method parameter of type java.security.Principal.

The controllers use the methods added to the repositories.

7.1. Spring Boot

springboot/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
import java.util.Optional;

@RestController (1)
@RequestMapping("/subscriptions") (2)
class SaasSubscriptionController {

    private final SaasSubscriptionRepository repository;

    private SaasSubscriptionController(SaasSubscriptionRepository repository) { (3)
        this.repository = repository;
    }

    @GetMapping("/{id}") (4)
    private ResponseEntity<SaasSubscription> findById(@PathVariable Long id,  (5)
                                                      Principal principal) { (6)
        return Optional.ofNullable(repository.findByIdAndOwner(id, principal.getName()))
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}
1 Annotate a class @RestController to identify the class as a Component capable of handling HTTP requests.
2 @RequestMapping identifies the request paths that invoke this Controller.
3 Use constructor injection to inject a bean of type SaasSubscriptionRepository.
4 The @GetMapping annotation maps the findById method to an HTTP GET request on /subscriptions/{id}.
5 Add the @PathVariable annotation to the handler method argument to make controller aware.
6 You can bind java.security.Principal as a method’s parameter in a controller.

7.2. Micronaut Framework

The application endpoints are only accessible to users with role SAAS_SUBSCRIPTION_OWNER. The Micronaut application annotates every controller with @Secured("SAAS_SUBSCRIPTION_OWNER"). The Spring Boot application specifies the role requirement in the SecurityConfig.java file.
micronautframework/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import java.security.Principal;
import io.micronaut.security.annotation.Secured;

@Controller("/subscriptions") (1)
@Secured("SAAS_SUBSCRIPTION_OWNER") (2)
class SaasSubscriptionController {

    private final SaasSubscriptionRepository repository;

    SaasSubscriptionController(SaasSubscriptionRepository repository) { (3)
        this.repository = repository;
    }

    @Get("/{id}") (4)
    HttpResponse<SaasSubscription> findById(@PathVariable Long id, (5)
                                            Principal principal) { (6)
        return repository.findByIdAndOwner(id, principal.getName())
                .map(HttpResponse::ok)
                .orElseGet(HttpResponse::notFound); (7)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions.
2 Annotate with io.micronaut.security.Secured to configure secured access. You can pass a list of roles which can can access.
3 Use constructor injection to inject a bean of type SaasSubscriptionRepository.
4 The @Get annotation maps the findById method to an HTTP GET request on /subscriptions/{id}.
5 You can define path variables with a RFC-6570 URI template in the HTTP Method annotation value. The method argument can optionally be annotated with @PathVariable.
6 You can bind java.security.Principal as a method’s parameter in a controller.
7 You can use the HTTPResponse fluid API to control the response’s status code, its body and headers.

8. Tests

In this tutorial, we use AssertJ in the tests.

8.1. Database Schema

The database schema should add owner column.

springboot/src/test/resources/schema.sql
CREATE TABLE IF NOT EXISTS saas_subscription
(
    id    BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name  VARCHAR(255) NOT NULL,
    cents NUMBER NOT NULL DEFAULT 0,
    owner VARCHAR(255) NOT NULL
);

8.2. Tests Seed Data

The seed data inserts entries with different owners.

springboot/src/test/resources/data.sql
INSERT INTO saas_subscription(id, name, cents, owner) VALUES (99, 'Advanced', 2900, 'sarah1');
INSERT INTO saas_subscription(id, name, cents, owner) VALUES (100, 'Essential', 1400, 'sarah1');
INSERT INTO saas_subscription(id, name, cents, owner) VALUES (101, 'Professional', 4900, 'sarah1');
INSERT INTO saas_subscription(id, name, cents, owner) VALUES (102, 'Enterprise', 9900, 'johnsnow');

8.3. Spring Boot Test

The following tests shows that you can use the method TestRestTemplate::withBasicAuth to supply basic authentication credentials.

springboot/src/test/java/example/micronaut/SecurityTest.java
package example.micronaut;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) (1)
class SecurityTest {

    @Autowired (2)
    TestRestTemplate restTemplate; (3)

    @Test
    void shouldNotAllowAccessToSaasSubscriptionsTheyDoNotOwn() {
        ResponseEntity<String> response = restTemplate
                .withBasicAuth("sarah1", "abc123") (4)
                .getForEntity("/subscriptions/102", String.class); // john's data
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    @Test
    void shouldRejectUsersWhoAreNotSubscriptionOwners() {
        ResponseEntity<String> response = restTemplate
                .withBasicAuth("john-owns-no-subscriptions", "qrs456")
                .getForEntity("/subscriptions/99", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    }

    @Test
    void shouldNotReturnASaasSubscriptionWithAnUnknownId() {
        ResponseEntity<String> response = restTemplate
                .withBasicAuth("sarah1", "BAD-PASSWORD")
                .getForEntity("/subscriptions/1000", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        assertThat(response.getBody()).isBlank();
    }
}
1 The @SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with @SpringBootApplication, for instance) and use that to start a Spring application context.
2 Inject a bean of type TestRestTemplate by using @Autowired on the field definition.
3 TestRestTemplate is a helper to ease to execution of HTTP requests against the locally running application.
4 basic authentication for sarah1.

8.4. Micronaut Test

The following tests shows that you can use the method MutableHttpRequest::basicAuth to supply basic authentication credentials.

micronautframework/src/test/java/example/micronaut/SecurityTest.java
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.test.annotation.Sql;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType;

@Sql(value = {"classpath:schema.sql", "classpath:data.sql"})
@MicronautTest (1)
class SecurityTest {

    @Test
    void shouldNotAllowAccessToSaasSubscriptionsTheyDoNotOwn(@Client("/") HttpClient httpClient) { (2)
        HttpRequest<?> request = HttpRequest.GET("/subscriptions/102")
                .basicAuth("sarah1", "abc123");
        HttpClientResponseException thrown = catchThrowableOfType(() ->
                httpClient.toBlocking().exchange(request, String.class), HttpClientResponseException.class);  (3)
        assertThat(thrown.getStatus().getCode()).isEqualTo(HttpStatus.NOT_FOUND.getCode());
    }

    @Test
    void shouldRejectUsersWhoAreNotSubscriptionOwners(@Client("/") HttpClient httpClient) { (2)
        HttpRequest<?> badPasswordRequest = HttpRequest.GET("/subscriptions/99")
                .basicAuth("john-owns-no-subscriptions", "qrs456");
        HttpClientResponseException badPasswordEx = catchThrowableOfType(() ->
                httpClient.toBlocking().exchange(badPasswordRequest, String.class), HttpClientResponseException.class);  (3)
        assertThat(badPasswordEx.getStatus().getCode()).isEqualTo(HttpStatus.FORBIDDEN.getCode());
    }

    @Test
    void shouldNotReturnASaasSubscriptionWithAnUnknownId(@Client("/") HttpClient httpClient) { (2)
        HttpRequest<?> request = HttpRequest.GET("/subscriptions/1000")
                .basicAuth("sarah1", "BAD-PASSWORD");
        HttpClientResponseException ex = catchThrowableOfType(() ->
                httpClient.toBlocking().exchange(request, String.class), HttpClientResponseException.class);  (3)
        assertThat(ex.getStatus().getCode()).isEqualTo(HttpStatus.UNAUTHORIZED.getCode());
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 When the HTTP Client receives a response with an HTTP Status Code >= 400, it throws an HttpClientResponseException. You can obtain the response status and body from the exception.

9. Conclusion

As you see in this tutorial, securing a REST API with basic authentication is straightforward in both Micronaut and Spring Boot. Security configuration differs between both frameworks but the coding experience of accessing the authenticated user as a Controller method parameter of type java.security.Principal is identical.

10. Next Steps

Learn more about Micronaut Security

11. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).