Micronaut Token Propagation

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

Authors: Sergio del Amo

Micronaut Version: 1.0.0.RC2

1 Getting Started

Lets describe the microservices you are going to build through the tutorial.

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

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

The next diagram illustrates the flow:

tokenpropagation

We generate valid JWT in the gateway microservice. Then every microservice in our app is able to validate those 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.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

1.1 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

1.2 Solution

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

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the App

We are going to write the app first without token propagation. Then we are going to configure token propagation and you will see how much code we can get rid of.

2.1 Gateway

Create the microservice:

mn create-app example.micronaut.gateway

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

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

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 io.micronaut.security.authentication.UserDetails;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;

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

@Singleton (1)
public class AuthenticationProviderUserPassword implements AuthenticationProvider { (2)

    @Override
    public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) {
        if ( (authenticationRequest.getIdentity().equals("sherlock") || authenticationRequest.getIdentity().equals("watson")) &&
                authenticationRequest.getSecret().equals("password") ) {
            return Flowable.just(new UserDetails((String) authenticationRequest.getIdentity(), Collections.emptyList()));
        }
        return Flowable.just(new AuthenticationFailed());
    }
}
1 To register a Singleton in Micronaut’s application context, annotate your class with javax.inject.Singleton.
2 A Micronaut’s Authentication Provider implements the interface io.micronaut.security.authentication.AuthenticationProvider.

Create a class UserController which exposes /user endpoint.

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.reactivex.Single;

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

    private final UsernameFetcher usernameFetcher;

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

    @Secured("isAuthenticated()")  (3)
    @Produces(MediaType.TEXT_PLAIN) (4)
    @Get (5)
    Single<String> index(@Header("Authorization") String authorization) {  (6)
        return usernameFetcher.findUsername(authorization);
    }
}
1 Annotate with io.micronaut.http.annotation.Controller to designate a class as a Micronaut’s 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’s 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.

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

import io.micronaut.http.annotation.Header;
import io.reactivex.Single;

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

Create a Micronaut HTTP Declarative client:

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.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Single;

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

    @Override
    @Get("/user") (3)
    Single<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.

Modify application.yml

gateway/src/main/resources/application.yml
micronaut:
    application:
        name: gateway
    server:
        port: 8080 (1)
    security:
        enabled: true (2)
        endpoints:
            login:
                enabled: true (3)
            oauth:
                enabled: true (4)
        token:
            jwt:
                enabled: true (5)
                signatures:
                    secret:
                        generator: (6)
                            secret: pleaseChangeThisSecretForANewOne (7)
    http:
        services:
            userecho: (8)
                urls:
                - "http://localhost:8081" (9)
1 Configure a fix port where the app listens.
2 Enable Micronaut’s security capabilities
3 Expose /login endpoint
4 Expose /oauth/access_token endpoint as defined by section 6 of the OAuth 2.0 spec - Refreshing an Access Token.
5 Enable JWT based authentication
6 You can create a SecretSignatureConfiguration named generator via configuration as illustrated above. The generator signature is used to sign the issued JWT claims.
7 Change this by your own secret and keep it safe (do not store this in your VCS)
8 This is the same service ID we used in the @Client annotation.
9 Configure a URL where the userecho microservice resides.

Tests

Provide a UsernameFetcher bean replacement for the Test environment.

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 io.reactivex.Single;

import javax.inject.Singleton;

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

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

Create a tests which verifies the app is secured and we can access it after login in:

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

import io.micronaut.context.ApplicationContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.security.authentication.UsernamePasswordCredentials;
import io.micronaut.security.token.jwt.render.BearerAccessRefreshToken;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;

public class UserControllerTest {
    private static EmbeddedServer server;
    private static HttpClient client;

    @BeforeClass
    public static void setupServer() {
        server = ApplicationContext.run(EmbeddedServer.class); (1)
        client = server
                .getApplicationContext()
                .createBean(HttpClient.class, server.getURL()); (2)
    }

    @AfterClass
    public static void stopServer() {
        if (server != null) {
            server.stop();
        }
        if (client != null) {
            client.stop();
        }
    }

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void testUserEndpointIsSecured() { (3)
        thrown.expect(HttpClientResponseException.class);
        thrown.expect(hasProperty("response", hasProperty("status", is(HttpStatus.UNAUTHORIZED))));
        client.toBlocking().exchange(HttpRequest.GET("/user"));
    }

    @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 To run the application from a unit test you can use the EmbeddedServer interface.
2 Register a RxClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
3 Test endpoint is secured

2.2 User Echo

Create the microservice:

mn create-app example.micronaut.userecho

Create a class UserController which exposes /user endpoint.

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 java.security.Principal;

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

    @Secured("isAuthenticated()") (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 a class as a Micronaut’s 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’s action responds to. To respond to a GET request, use the io.micronaut.http.annotation.Get annotation.
5 If a user is authenticated, Micronaut will bind the user object to an argument of type java.security.Principal (if present).

Modify application.yml

userecho/src/main/resources/application.yml
micronaut:
    application:
        name: userecho
    server:
        port: 8081 (1)
    security:
        enabled: true (2)
        token:
            jwt:
                enabled: true (3)
                signatures:
                    secret: (4)
                        validation:
                            secret: pleaseChangeThisSecretForANewOne (5)
1 Configure a fix port where the app listens.
2 Enable Micronaut’s security capabilities
3 Enable JWT based authentication
4 You can create a SecretSignatureConfiguration named validation which is able to validate JWT generated by the gateway microservice.
5 Change this by your own secret and keep it safe (do not store this in your VCS)

2.3 Token Propagation

As you can see propagating of the JWT token to other microservices in our app 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 app with several controllers and declarative clients, it can lead to a lot of repetition. Fortunately, Micronaut includes a feature called token propagation. We can tell our app 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:
    ...
    ..
    ..
            writer:
                header:
                    enabled: true (1)
            propagation:
                enabled: true (2)
                service-id-regex: "userecho" (3)
....
..
1 Micronaut ships with several TokenWriter beans which you can enable. In this example, we enable the HTTP header token writer which propagates the token in the HTTP Header Authorization. You can code your own TokenWriter to satisfy your requirements.
2 Enable token propagation
3 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.Header;
import io.micronaut.http.annotation.Produces;
import io.micronaut.security.annotation.Secured;
import io.reactivex.Single;

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

    private final UsernameFetcher usernameFetcher;

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

    @Secured("isAuthenticated()")
    @Produces(MediaType.TEXT_PLAIN)
    @Get
    Single<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 io.reactivex.Single;

public interface UsernameFetcher {
    Single<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.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Single;

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

    @Get("/user")
    Single<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 io.micronaut.http.annotation.Header;
import io.reactivex.Single;

import javax.inject.Singleton;

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

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

3 Running the App

Run both microservices:

userecho $ ./gradlew run
> Task :complete/userecho:run
18:29:26.500 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 671ms. Server Running: http://localhost:8081
<=========----> 75% EXECUTING [10s]
gateway $ ./gradlew run

> Task :complete/gateway:run
18:28:35.723 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 707ms. Server Running: http://localhost:8080

Do cURL command to authenticate:

curl -X "POST" "http://localhost:8080/login" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "username": "sherlock",
  "password": "password"
}'

response:

{"username":"sherlock","access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzNzk3OTQxNSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM3OTgzMDE1LCJpYXQiOjE1Mzc5Nzk0MTV9.HkMhguhW-cbT7u_3vL-eWxn9MbPgR1vTRjDYqvfl8Vc","refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzNzk3OTQxNSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiaWF0IjoxNTM3OTc5NDE1fQ.LNpA-v45_6xPCB0MOMXd9QArWhCJS8C0AYpRj6Kj4-E","expires_in":3600,"token_type":"Bearer"}

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

`curl "http://localhost:8080/user" -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzNzk3OTQxNSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM3OTgzMDE1LCJpYXQiOjE1Mzc5Nzk0MTV9.HkMhguhW-cbT7u_3vL-eWxn9MbPgR1vTRjDYqvfl8Vc'`
sherlock

4 Next Steps

Read more about Token Propagation and Security inside Micronaut.