implementation("org.springframework.boot:spring-boot-starter-security")
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:
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.
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
package example.micronaut;
import org.springframework.data.annotation.Id;
record SaasSubscription(@Id Long id, String name, Integer cents, String owner) {
}
4.2. Micronaut Framework
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
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
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:
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.
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
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.
|
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.
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.
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.
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.
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…). |