DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Designing for Security
  • What Is API-First?
  • Externalize Microservice Configuration With Spring Cloud Config
  • Create a Multi-Tenancy Application in Nest.js, Part 4: Authentication and Authorization Setup

Trending

  • From Fragmentation to Focus: A Data-First, Team-First Framework for Platform-Driven Organizations
  • Key Considerations in Cross-Model Migration
  • Unlocking the Potential of Apache Iceberg: A Comprehensive Analysis
  • CRDTs Explained: How Conflict-Free Replicated Data Types Work
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Micronaut Tutorial: Security

Micronaut Tutorial: Security

Learn how to add security protocols, like authentication and authorization, to your microservices with Micronaut.

By 
Piotr Mińkowski user avatar
Piotr Mińkowski
·
Apr. 29, 19 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
19.1K Views

Join the DZone community and get the full member experience.

Join For Free
Microservices Security

This is the third part of my tutorial on the Micronaut Framework. This time we will discuss the most interesting Micronaut security features. I have already described the core mechanisms for IoC and dependency injection in the first part of my tutorial, and I have also created a guide to building simple REST server-side application in the second part. For more details you may refer to:

  • Part 1: Micronaut Tutorial: Beans and Scopes
  • Part 2: Micronaut Tutorial: Server Application

Security is as essential part of every web application. Easily configurable, built-in web security mechanisms is something that every single modern micro-framework must have. It is no different with Micronaut. In this part of my tutorial, you will learn how to:

  • Build custom authentication providers.
  • Configure and test basic authentication for your HTTP API.
  • Secure your HTTP API using JSON Web Tokens.
  • Enable communication over HTTPS.

Enabling Security

To enable security for Micronaut applications, you should first include the following dependency in your pom.xml:

<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-security</artifactId>
</dependency>

The next step is to enable security feature through application properties:

micronaut:
  security:
    enabled: true

Setting the property micronaut.security.enabled to true enables security for all the existing controllers. Because we already have the controller, that has been used as an example for the previous part of tutorial, we should disable security for it. To do that, I have annotated it with @Secured(SecurityRule.IS_ANONYMOUS). This allows anonymous access to all endpoints implemented inside controller.

@Controller("/persons")
@Secured(SecurityRule.IS_ANONYMOUS)
@Validated
public class PersonController { ... }

Basic Authentication Provider

Once you enabled Micronaut security, basic auth is enabled by default. All you need to do is to implement your custom authentication provider. It has to implement the AuthenticationProvider interface. In fact, you just need to verify the username and password, which are both passed inside the HTTP Authorization header. Our sample authentication provider uses configuration properties as a user repository. Here's the fragment of the application.yml file that contains a list of user passwords and assigned roles:

credentials:
  users:
    smith: smith123
    scott: scott123
    piomin: piomin123
    test: test123
  roles:
    smith: ADMIN
    scott: VIEW
    piomin: VIEW
    test: ADMIN

The configuration properties are injected into the UsersStore configuration bean which is annotated with @ConfigurationProperties. User passwords are stored inside the users map, while roles are inside the roles map. They are both annotated with @MapFormat and have username as a key.

ConfigurationProperties("credentials")
public class UsersStore {

    @MapFormat
    Map<String, String> users;
    @MapFormat
    Map<String, String> roles;

    public String getUserPassword(String username) {
        return users.get(username);
    }

    public String getUserRole(String username) {
        return roles.get(username);
    }
}

Finally, we may proceed to the authentication provider implementation. It injects a UsersStore bean that contains a list of users with passwords and roles. The overridden method should return the UserDetails object. The username and password are automatically decoded from base64 taken from the Authentication header and bound to the identity and secret fields in theAuthenticationRequest method parameter. If the input password is the same as the stored password it returns the UserDetails object with roles, otherwise, it throws an exception.

@Singleton
public class UserPasswordAuthProvider implements AuthenticationProvider {

    @Inject
    UsersStore store;

    @Override
    public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest req) {
        String username = req.getIdentity().toString();
        String password = req.getSecret().toString();
        if (password.equals(store.getUserPassword(username))) {
            UserDetails details = new UserDetails(username, Collections.singletonList(store.getUserRole(username)));
            return Flowable.just(details);
        } else {
            return Flowable.just(new AuthenticationFailed());
        }
    }
}

Secured Controller

Now, we may create our sample secure REST controller. The following controller is just a copy of the previously described controller, PersonController, but it also contains some Micronaut Security annotations. Through @Secured(SecurityRule.IS_AUTHENTICATED), which we've used on the whole controller, the app is now available only for succesfully authenticated users. This annotation may be overridden at the method level. The method for adding a new person is available only for users with an ADMIN role.

@Controller("/secure/persons")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class SecurePersonController {

    List<Person> persons = new ArrayList<>();

    @Post
    @Secured("ADMIN")
    public Person add(@Body @Valid Person person) {
        person.setId(persons.size() + 1);
        persons.add(person);
        return person;
    }

    @Get("/{id:4}")
    public Optional<Person> findById(@NotNull Integer id) {
        return persons.stream()
                .filter(it -> it.getId().equals(id))
                .findFirst();
    }

    @Version("1")
    @Get("{?max,offset}")
    public List<Person> findAll(@Nullable Integer max, @Nullable Integer offset) {
        return persons.stream()
                .skip(offset == null ? 0 : offset)
                .limit(max == null ? 10000 : max)
                .collect(Collectors.toList());
    }

    @Version("2")
    @Get("?max,offset")
    public List<Person> findAllV2(@NotNull Integer max, @NotNull Integer offset) {
        return persons.stream()
                .skip(offset == null ? 0 : offset)
                .limit(max == null ? 10000 : max)
                .collect(Collectors.toList());
    }

}

To test Micronaut security features used in our controller we will create JUnit test classes containing three methods. All these methods use Micronaut HTTP client for calling target endpoints. It provides abasicAuth method, that allows you to easily pass user credentials. The first test method testAdd verifies positive scenarios for adding a new person. The test user smith has an ADMIN role, which is required for calling this HTTP endpoint. In contrast, the method testAddFailed calls the same HTTP endpoint, but with a different user, scott, who has a VIEW role. We expect that HTTP 401 is returned by the endpoint. The same user, scott, has access to GET endpoints, so we expect that test method testFindById is successful.

@MicronautTest
public class SecurePersonControllerTests {

    @Inject
    EmbeddedServer server;

    @Test
    public void testAdd() throws MalformedURLException {
        HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
        Person person = new Person();
        person.setFirstName("John");
        person.setLastName("Smith");
        person.setAge(33);
        person.setGender(Gender.MALE);
        person = client.toBlocking()
                .retrieve(HttpRequest.POST("/secure/persons", person).basicAuth("smith", "smith123"), Person.class);
        Assertions.assertNotNull(person);
        Assertions.assertEquals(Integer.valueOf(1), person.getId());
    }

    @Test
    public void testAddFailed() throws MalformedURLException {
        HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
        Person person = new Person();
        person.setFirstName("John");
        person.setLastName("Smith");
        person.setAge(33);
        person.setGender(Gender.MALE);
        Assertions.assertThrows(HttpClientResponseException.class,
                () -> client.toBlocking().retrieve(HttpRequest.POST("/secure/persons", person).basicAuth("scott", "scott123"), Person.class),
                "Forbidden");
    }

    @Test
    public void testFindById() throws MalformedURLException {
        HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
        Person person = client.toBlocking()
                .retrieve(HttpRequest.GET("/secure/persons/1").basicAuth("scott", "scott123"), Person.class);
        Assertions.assertNotNull(person);
    }
}

Enable HTTPS

Our controller is secured, but not the HTTP server. Micronaut, by default, starts the server with SSL disabled. However, it supports HTTPS out of the box. To enable HTTPS support, you should first set themicronaut.ssl.enabled property to true. By default Micronaut with HTTPS enabled starts on port 8443, but you can override it using the micronaut.ssl.port property.
We will enable HTTPS only for a single JUnit test class. To do that we first create file src/test/resources/ssl.yml with the following configuration:

micronaut:
  ssl:
    enabled: true
    buildSelfSigned: true

Micronaut simplifies SSL configuration builds for test purposes. It turns out, we don't have to generate any keystores or certificates if we use the micronaut.ssl.buildSelfSigned property. Otherwise, you would have to generate a keystore by yourself. It is not difficult, if you are creating a self-signed certificate. You may use openssl or keytool for that. Here's the appropriate keytool command for generating a keystore, however, I should point out that the tool by Micronautis openssl:

$ keytool -genkey -alias server -keystore server.jks

If you decide to generate a self-signed certificate by yourself you have configure them:

micronaut:
  ssl:
    enabled: true
    keyStore:
      path: classpath:server.keystore
      password: 123456
      type: JKS

The last step is to create JUnit tests that use the configuration provided in ssl.yml file.

@MicronautTest(propertySources = "classpath:ssl.yml")
public class SecureSSLPersonControllerTests {

    @Inject
    EmbeddedServer server;

    @Test
    public void testFindById() throws MalformedURLException {
        HttpClient client = HttpClient.create(new URL(server.getScheme() + "://" + server.getHost() + ":" + server.getPort()));
        Person person = client.toBlocking()
                .retrieve(HttpRequest.GET("/secure/persons/1").basicAuth("scott", "scott123"), Person.class);
        Assertions.assertNotNull(person);
    }

    // other tests ...

}

JWT Authentication

To enable JWT token-based authentication, we first need to include the following dependency inpom.xml:

<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-security-jwt</artifactId>
</dependency>

Token authentication is enabled by default through the TokenConfigurationProperties properties ( micronaut.security.token.enabled). However, we should enable JWT-based authentication by setting the micronaut.security.token.jwt.enabled property to true. This change allows us to use JWT authentication for our sample application. We also need to be able to generate authentication tokens used for authorization. To do that, we should enable the /login endpoint and set some configuration properties for a JWT token generator. In the following fragment of the application.yml file, I set HMAC with SHA-256 as the hash algorithm for a JWT signature generator:

micronaut:
  security:
    enabled: true
    endpoints:
      login:
        enabled: true
    token:
      jwt:
        enabled: true
        signatures:
          secret:
            generator:
              secret: pleaseChangeThisSecretForANewOne
              jws-algorithm: HS256

Now, we can call the POST /login endpoint with the username and password in the JSON body as shown below:

$ curl -X "POST" "http://localhost:8100/login" -H 'Content-Type: application/json; charset=utf-8' -d '{"username":"smith","password":"smith123"}'
{
    "username": "smith",
    "roles": [
        "ADMIN"
    ],
    "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzbWl0aCIsIm5iZiI6MTU1NjE5ODAyMCwicm9sZXMiOlsiQURNSU4iXSwiaXNzIjoic2FtcGxlLW1pY3JvbmF1dC1hcHBsaWNhdGlvbiIsImV4cCI6MTU1NjIwMTYyMCwiaWF0IjoxNTU2MTk4MDIwfQ.by0Dx73QIZeF4MDM4A5nHgw8xm4haPJjsu9z45psQrY",
    "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzbWl0aCIsIm5iZiI6MTU1NjE5ODAyMCwicm9sZXMiOlsiQURNSU4iXSwiaXNzIjoic2FtcGxlLW1pY3JvbmF1dC1hcHBsaWNhdGlvbiIsImlhdCI6MTU1NjE5ODAyMH0.2BrdZzuvJNymZlOv56YpUPHYLDdnVAW5UXXNuz3a7xU",
    "token_type": "Bearer",
    "expires_in": 3600
}

The value of the access_token field returned in the response should be passed as a bearer token in the Authorization header of the requests sent to HTTP endpoints. We can use any endpoint, for example GET /persons

$ curl -X "GET" "http://localhost:8100/persons" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzbWl0aCIsIm5iZiI6MTU1NjE5ODAyMCwicm9sZXMiOlsiQURNSU4iXSwiaXNzIjoic2FtcGxlLW1pY3JvbmF1dC1hcHBsaWNhdGlvbiIsImV4cCI6MTU1NjIwMTYyMCwiaWF0IjoxNTU2MTk4MDIwfQ.by0Dx73QIZeF4MDM4A5nHgw8xm4haPJjsu9z45psQrY"

We can easily and automatically test the scenario described above. I have created UserCredentials and UserToken objects for serializing requests and deserializing responses from the /login endpoint. The token retrieved from response is then passed as a bearer token by calling the bearerAuth method on the Micronaut HTTP client instance.

@MicronautTest
public class SecurePersonControllerTests {

    @Inject
    EmbeddedServer server;

    @Test
    public void testFindByIdUsingJWTToken() throws MalformedURLException {
        HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
        UserToken token = client.toBlocking().retrieve(HttpRequest.POST("/login", new User Credentials("scott", "scott123")), UserToken.class);
        Person person = client.toBlocking()
                .retrieve(HttpRequest.GET("/secure/persons/1").bearerAuth(token.getAccessToken()), Person.class);
        Assertions.assertNotNull(person);
    }
}

Source Code

We were using the same repository as for two previous parts of my Micronaut tutorial: https://github.com/piomin/sample-micronaut-applications.git.

authentication Property (programming) Testing

Published at DZone with permission of Piotr Mińkowski, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Designing for Security
  • What Is API-First?
  • Externalize Microservice Configuration With Spring Cloud Config
  • Create a Multi-Tenancy Application in Nest.js, Part 4: Authentication and Authorization Setup

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!