Micronaut Token Propagation

Learn how to leverage token propagation in the Micronaut framework to simplify your code while keeping your microservices secure.

Authors: Sergio del Amo

Micronaut Version: 3.2.7

1. Getting Started

Let’s describe the microservices you will build through the guide.

  • gateway - A microservice secured via JWT which exposes an endpoint /user. The output of that endpoint is the result of consuming the userecho endpoint.

  • userecho - A microservice secured via JWT which exposes an endpoint /user which responds with the username of the authenticated user.

The next diagram illustrates the flow:

tokenpropagation

We generate a valid JWT in the gateway microservice. Then every microservice in our application is able to validate this JWT. We want every internal request to contain a valid JWT token. If we want to talk to another microservice we need to propagate the valid JWT get received.

1.1. Enable annotation Processing

If you use Java or Kotlin and IntelliJ IDEA, make sure to enable annotation processing.

annotationprocessorsintellij

2. What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Writing the Application

We will write the application first without token propagation. Then we will configure token propagation, and you will see how much code we can remove.

4.1. Gateway

Create the microservice:

mn create-app example.micronaut.gateway --build=maven --lang=java

Add the security-jwt module to the configuration:

pom.xml
<!-- Add the following to your annotationProcessorPaths element -->
<path>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-annotations</artifactId>
</path>
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-jwt</artifactId>
    <scope>compile</scope>
</dependency>

To keep this guide simple, create a naive AuthenticationProvider to simulate user’s authentication.

intermediate-gateway/src/main/java/example/micronaut/AuthenticationProviderUserPassword.java
package example.micronaut;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.AuthenticationException;
import io.micronaut.security.authentication.AuthenticationFailed;
import io.micronaut.security.authentication.AuthenticationProvider;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Flux;
import org.reactivestreams.Publisher;

import jakarta.inject.Singleton;
import java.util.Collections;

@Singleton (1)
public class AuthenticationProviderUserPassword implements AuthenticationProvider { (2)
    
    @Override
    public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) {
        return Flux.create(emitter -> {
            if ((authenticationRequest.getIdentity().equals("sherlock") || authenticationRequest.getIdentity().equals("watson")) &&
                    authenticationRequest.getSecret().equals("password")) {
                emitter.next(AuthenticationResponse.success((String) authenticationRequest.getIdentity()));
                emitter.complete();
            } else {
                emitter.error(AuthenticationResponse.exception());
            }
        }, FluxSink.OverflowStrategy.ERROR);

    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.
2 A Micronaut Authentication Provider implements the interface io.micronaut.security.authentication.AuthenticationProvider.

Create a class UserController which exposes /user endpoint.

intermediate-gateway/src/main/java/example/micronaut/UserController.java
package example.micronaut;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.annotation.Produces;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

@Controller("/user") (1)
public class UserController {

    private final UsernameFetcher usernameFetcher;

    public UserController(UsernameFetcher usernameFetcher) {  (2)
        this.usernameFetcher = usernameFetcher;
    }

    @Secured(SecurityRule.IS_AUTHENTICATED)  (3)
    @Produces(MediaType.TEXT_PLAIN) (4)
    @Get (5)
    Mono<String> index(@Header("Authorization") String authorization) {  (6)
        return usernameFetcher.findUsername(authorization);
    }
}
1 Annotate with io.micronaut.http.annotation.Controller to designate the class as a Micronaut controller.
2 Constructor dependency injection
3 Annotate with io.micronaut.security.Secured to configure secured access. The isAuthenticated() expression will allow access only to authenticated users.
4 Since we return a string which is not valid JSON, set the media type to text/plain.
5 You can specify the HTTP verb that a controller action responds to. To respond to a GET request, use the io.micronaut.http.annotation.Get annotation.
6 You can bind an HTTP header to a controller method argument.

Create an interface to encapsulate the collaboration with the userecho microservice.

intermediate-gateway/src/main/java/example/micronaut/UsernameFetcher.java
package example.micronaut;

import io.micronaut.http.annotation.Header;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

public interface UsernameFetcher {
    Mono<String> findUsername(@Header("Authorization") String authorization);
}

Create a Micronaut HTTP Declarative client:

intermediate-gateway/src/main/java/example/micronaut/UserEchoClient.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

@Client(id = "userecho") (1)
@Requires(notEnv = Environment.TEST) (2)
public interface UserEchoClient extends UsernameFetcher {

    @Override
    @Consumes(MediaType.TEXT_PLAIN)
    @Get("/user") (3)
    Mono<String> findUsername(@Header("Authorization") String authorization); (4)
}
1 The @Client annotation is used with a service id. We will reference the exact service id in the configuration shortly.
2 Don’t load this bean in the Test environment.
3 Use @Get annotation to define the client mapping
4 Supply the JWT to the HTTP Authorization header value to the @Client method.

Add this snippet to application.yml to configure the service URL of the echo service

intermediate-gateway/src/main/resources/application.yml
micronaut:
  http:
    services:
      userecho: (1)
        urls:
          - "http://localhost:8081" (2)
1 This is the same service ID we used in the @Client annotation.
2 Configure a URL where the userecho microservice resides.

Add this snippet to application.yml to configure security:

intermediate-gateway/src/main/resources/application.yml
micronaut:
  security:
    authentication: bearer (1)
    token:
      jwt:
        signatures:
          secret:
            generator: (2)
              secret: '"${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}"' (3)
1 Set authentication to bearer to receive a JSON response from the login endpoint.
2 You can create a SecretSignatureConfiguration named generator via configuration as illustrated above. The generator signature is used to sign the issued JWT claims.
3 Change this to your own secret and keep it safe (do not store this in your VCS)

4.1.1. Tests

Provide a UsernameFetcher bean replacement for the Test environment.

intermediate-gateway/src/test/java/example/micronaut/UserEchoClientReplacement.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.http.annotation.Header;
import org.reactivestreams.Publisher;
import jakarta.inject.Singleton;
import reactor.core.publisher.Mono;

@Requires(env = Environment.TEST)
@Singleton
public class UserEchoClientReplacement implements UsernameFetcher {

    @Override
    public Mono<String> findUsername(@Header("Authorization") String authorization) {
        return Mono.just("sherlock");
    }
}

Create tests to verify the application is secured and we can access it after login:

intermediate-gateway/src/test/java/example/micronaut/UserControllerTest.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.security.authentication.UsernamePasswordCredentials;
import io.micronaut.security.token.jwt.render.BearerAccessRefreshToken;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@MicronautTest (1)
public class UserControllerTest {

    @Inject
    @Client("/")
    HttpClient client; (2)

    @Test
    public void testUserEndpointIsSecured() { (3)
        HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, () -> {
            client.toBlocking().exchange(HttpRequest.GET("/user"));
        });

        assertEquals(HttpStatus.UNAUTHORIZED, thrown.getResponse().getStatus());
    }

    @Test
    public void testAuthenticatedCanFetchUsername() {
        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("sherlock", "password");
        HttpRequest request = HttpRequest.POST("/login", credentials);

        BearerAccessRefreshToken bearerAccessRefreshToken = client.toBlocking().retrieve(request, BearerAccessRefreshToken.class);

        String username = client.toBlocking().retrieve(HttpRequest.GET("/user")
                .header("Authorization", "Bearer " + bearerAccessRefreshToken.getAccessToken()), String.class);

        assertEquals("sherlock", username);
    }
}
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 Test endpoint is secured

4.2. User echo

Create the microservice:

mn create-app example.micronaut.userecho --build=maven --lang=java

Add the security-jwt module to the configuration:

pom.xml
<!-- Add the following to your annotationProcessorPaths element -->
<path>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-annotations</artifactId>
</path>
<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-jwt</artifactId>
    <scope>compile</scope>
</dependency>

Create a class UserController which exposes /user endpoint.

intermediate-userecho/src/main/java/example/micronaut/UserController.java
package example.micronaut;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

import java.security.Principal;

@Controller("/user") (1)
public class UserController {

    @Secured(SecurityRule.IS_AUTHENTICATED) (2)
    @Produces(MediaType.TEXT_PLAIN) (3)
    @Get (4)
    String index(Principal principal) { (5)
        return principal.getName();
    }
}
1 Annotate with io.micronaut.http.annotation.Controller to designate the class as a Micronaut controller.
2 Annotate with io.micronaut.security.Secured to configure secured access. The isAuthenticated() expression will allow access only to authenticated users.
3 Since we return a string which is not valid JSON, set the media type to text/plain.
4 You can specify the HTTP verb that a controller action responds to. To respond to a GET request, use the io.micronaut.http.annotation.Get annotation.
5 If a user is authenticated, the Micronaut framework will bind the user object to an argument of type java.security.Principal (if present).

Add this snippet to application.yml to change the port where userecho starts:

intermediate-userecho/src/main/resources/application.yml
micronaut:
  server:
    port: 8081 (1)
1 Configure the port where the application listens.

Add this snippet to application.yml

intermediate-userecho/src/main/resources/application.yml
micronaut:
  security:
    token:
      jwt:
        signatures:
          secret:
            validation: (1)
              secret: '"${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}"' (2)
1 You can create a SecretSignatureConfiguration named validation which is able to validate JWT generated by the gateway microservice.
2 Change this to your own secret and keep it safe (do not store this in your VCS)

4.3. Token Propagation

As you can see, propagating the JWT token to other microservices in our application complicates the code. We need to capture the Authorization header in the controller method arguments and then pass it to the @Client bean. In an application with several controllers and declarative clients, it can lead to a lot of repetition. Fortunately, the Framework includes a feature called token propagation. We can tell our application to propagate the incoming token to a set of outgoing requests.

Let’s configure token propagation. We need to modify application.yml in the gateway microservice:

gateway/src/main/resources/application.yml
micronaut:
  security:
    token:
      propagation:
        enabled: true (1)
        service-id-regex: "userecho" (2)
1 Enable token propagation
2 We only want to propagate the token to certain services. We can create a regular expression to match those services ids.

We can simplify the code:

Edit UserController.java and remove the @Header parameter:

gateway/src/main/java/example/micronaut/UserController.java
package example.micronaut;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

@Controller("/user")
public class UserController {

    private final UsernameFetcher usernameFetcher;

    public UserController(UsernameFetcher usernameFetcher) {
        this.usernameFetcher = usernameFetcher;
    }

    @Secured(SecurityRule.IS_AUTHENTICATED)
    @Produces(MediaType.TEXT_PLAIN)
    @Get
    Mono<String> index() {
        return usernameFetcher.findUsername();
    }
}

Edit UsernameFetcher.java and remove the @Header parameter:

gateway/src/main/java/example/micronaut/UsernameFetcher.java
package example.micronaut;

import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

public interface UsernameFetcher {
    Mono<String> findUsername();
}

Edit UserEchoClient.java and remove the @Header parameter:

gateway/src/main/java/example/micronaut/UserEchoClient.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

@Client(id = "userecho")
@Requires(notEnv = Environment.TEST)
public interface UserEchoClient extends UsernameFetcher {

    @Consumes(MediaType.TEXT_PLAIN)
    @Get("/user")
    Mono<String> findUsername();
}

Edit UserEchoClientReplacement.java and remove the @Header parameter:

gateway/src/test/java/example/micronaut/UserEchoClientReplacement.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import jakarta.inject.Singleton;

@Requires(env = Environment.TEST)
@Singleton
public class UserEchoClientReplacement implements UsernameFetcher {

    @Override
    public Mono<String> findUsername() {
        return Mono.just("sherlock");
    }
}

5. Running the App

Run both microservices:

userecho
./mvnw mn:run
18:29:26.500 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 671ms. Server Running: http://localhost:8081
gateway
./mvnw mn:run
18:28:35.723 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 707ms. Server Running: http://localhost:8080

Send a curl request to authenticate:

curl -X "POST" "http://localhost:8080/login" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{"username": "sherlock", "password": "password"}'
{"username":"sherlock","access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTYxNTkxMDM3Nywicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNjE1OTEzOTc3LCJpYXQiOjE2MTU5MTAzNzd9.nWoaNq9YzRzYKDBvDw_QaiUyVyIoc6rHCW_vLfnrtQ8","token_type":"Bearer","expires_in":3600}

Now you can call the /user endpoint supplying the access token in the Authorization header.

curl "http://localhost:8080/user" -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTYxNTkxMDM3Nywicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNjE1OTEzOTc3LCJpYXQiOjE2MTU5MTAzNzd9.nWoaNq9YzRzYKDBvDw_QaiUyVyIoc6rHCW_vLfnrtQ8'
sherlock

6. Generate a Micronaut Application Native Image with GraalVM

We will use GraalVM, the polyglot embeddable virtual machine, to generate a native image of our Micronaut application.

Compiling native images ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.

Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.

6.1. Native image generation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 11
$ sdk install java 21.3.0.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM.
Java 17
$ sdk install java 21.3.0.r17-grl

For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.

After installing GraalVM, install the native-image component, which is not installed by default:

gu install native-image

To generate a native image using Maven, run:

./mvnw package -Dpackaging=native-image

The native image is created in the target directory and can be run with target/application.

After creating the native images for both microservices, start them and send the same curl requests as before to check that everything works using GraalVM native images.

7. Next steps

Read more about Token Propagation and Micronaut Security.

8. Help with the Micronaut Framework

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.