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.9.2
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 theuserecho
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:
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.
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.
-
Download and unzip the source
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=groovy
Add the security-jwt module to the configuration:
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-annotations</artifactId>
<scope>compile</scope>
</dependency>
<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.
package example.micronaut
import io.micronaut.core.annotation.Nullable
import io.micronaut.http.HttpRequest
import io.micronaut.security.authentication.AuthenticationProvider
import io.micronaut.security.authentication.AuthenticationRequest
import io.micronaut.security.authentication.AuthenticationResponse
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.FluxSink
@Singleton (1)
class AuthenticationProviderUserPassword implements AuthenticationProvider { (2)
@Override
Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest,
AuthenticationRequest<?, ?> authenticationRequest) {
return Flux.create(emitter -> {
if ((authenticationRequest.identity == 'sherlock' || authenticationRequest.identity == 'watson') &&
authenticationRequest.secret == 'password') {
emitter.next(AuthenticationResponse.success((String) authenticationRequest.identity))
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.
package example.micronaut
import groovy.transform.CompileStatic
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 reactor.core.publisher.Mono
import static io.micronaut.http.MediaType.TEXT_PLAIN
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED
@CompileStatic
@Controller('/user') (1)
class UserController {
private final UsernameFetcher usernameFetcher
UserController(UsernameFetcher usernameFetcher) { (2)
this.usernameFetcher = usernameFetcher
}
@Secured(IS_AUTHENTICATED) (3)
@Produces(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.
package example.micronaut
import io.micronaut.http.annotation.Header
import reactor.core.publisher.Mono
interface UsernameFetcher {
Mono<String> findUsername(@Header('Authorization') String authorization)
}
Create a Micronaut HTTP Declarative client:
package example.micronaut
import io.micronaut.context.annotation.Requires
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 reactor.core.publisher.Mono
import static io.micronaut.context.env.Environment.TEST
import static io.micronaut.http.MediaType.TEXT_PLAIN
@Client(id = 'userecho') (1)
@Requires(notEnv = TEST) (2)
interface UserEchoClient extends UsernameFetcher {
@Override
@Consumes(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
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:
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.
package example.micronaut
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Requires
import io.micronaut.http.annotation.Header
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
import static io.micronaut.context.env.Environment.TEST
@CompileStatic
@Requires(env = TEST)
@Singleton
class UserEchoClientReplacement implements UsernameFetcher {
@Override
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:
package example.micronaut
import io.micronaut.http.HttpRequest
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.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification
import static io.micronaut.http.HttpStatus.UNAUTHORIZED
@MicronautTest (1)
class UserControllerSpec extends Specification {
@Inject
@Client('/')
HttpClient client (2)
void testUserEndpointIsSecured() { (3)
when:
client.toBlocking().exchange(HttpRequest.GET('/user'))
then:
HttpClientResponseException e = thrown()
UNAUTHORIZED == e.response.status
}
void testAuthenticatedCanFetchUsername() {
when:
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials('sherlock', 'password')
HttpRequest<?> request = HttpRequest.POST('/login', credentials)
BearerAccessRefreshToken bearerAccessRefreshToken = client.toBlocking().retrieve(request, BearerAccessRefreshToken)
String username = client.toBlocking().retrieve(HttpRequest.GET('/user')
.header('Authorization', 'Bearer ' + bearerAccessRefreshToken.getAccessToken()), String)
then:
'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=groovy
Add the security-jwt module to the configuration:
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-annotations</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
<scope>compile</scope>
</dependency>
Create a class UserController
which exposes /user
endpoint.
package example.micronaut
import groovy.transform.CompileStatic
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
import static io.micronaut.http.MediaType.TEXT_PLAIN
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED
@CompileStatic
@Controller('/user') (1)
class UserController {
@Secured(IS_AUTHENTICATED) (2)
@Produces(TEXT_PLAIN) (3)
@Get(4)
String index(Principal principal) { (5)
return principal.name
}
}
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:
micronaut:
server:
port: 8081 (1)
1 | Configure the port where the application listens. |
Add this snippet to 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:
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:
package example.micronaut
import groovy.transform.CompileStatic
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 reactor.core.publisher.Mono
import static io.micronaut.http.MediaType.TEXT_PLAIN
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED
@CompileStatic
@Controller('/user')
class UserController {
private final UsernameFetcher usernameFetcher
UserController(UsernameFetcher usernameFetcher) {
this.usernameFetcher = usernameFetcher
}
@Secured(IS_AUTHENTICATED)
@Produces(TEXT_PLAIN)
@Get
Mono<String> index() {
return usernameFetcher.findUsername()
}
}
Edit UsernameFetcher.java
and remove the @Header
parameter:
package example.micronaut
import reactor.core.publisher.Mono
interface UsernameFetcher {
Mono<String> findUsername()
}
Edit UserEchoClient.java
and remove the @Header
parameter:
package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.http.annotation.Consumes
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import reactor.core.publisher.Mono
import static io.micronaut.context.env.Environment.TEST
import static io.micronaut.http.MediaType.TEXT_PLAIN
@Client(id = 'userecho')
@Requires(notEnv = TEST)
interface UserEchoClient extends UsernameFetcher {
@Consumes(TEXT_PLAIN)
@Get('/user')
Mono<String> findUsername()
}
Edit UserEchoClientReplacement.java
and remove the @Header
parameter:
package example.micronaut
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
import static io.micronaut.context.env.Environment.TEST
@CompileStatic
@Requires(env = TEST)
@Singleton
class UserEchoClientReplacement implements UsernameFetcher {
@Override
Mono<String> findUsername() {
return Mono.just('sherlock')
}
}
5. Running the App
Run both microservices:
./mvnw mn:run
18:29:26.500 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 671ms. Server Running: http://localhost:8081
./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. Next steps
Read more about Token Propagation and Micronaut Security.
7. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.