Client Credentials Flow with Micronaut and Auth0

Learn how to use Client Credentials Flow between Micronaut microservices with an Authorization Server provided by Auth0.

Authors: Sergio del Amo

Micronaut Version: 3.2.7

1. Getting Started

In this guide, we will create a Micronaut application written in Kotlin.

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

  • An Auth0 account.

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. Application Diagram

Download the complete solution of the Consul and Micronaut Framework - Microservices Service Discovery guide. You will use the sample app as a starting point. The application contains three microservices:

  • bookcatalogue - This returns a list of books. It uses a domain consisting of a book name and an ISBN.

  • bookinventory - This exposes an endpoint to check whether a book has sufficient stock to fulfil an order. It uses a domain consisting of a stock level and an ISBN.

  • bookrecommendation - This consumes previous services and exposes an endpoint that recommends book names that are in stock.

The bookcatalogue service consumes endpoints exposed by the other services. The following image illustrates the original application flow:

flow

A request to bookrecommendation (http://localhost:8080/books) triggers several requests through our microservices mesh.

In this guide, you are going to secure the communication between the microservices. You will use a client credentials flow and obtain an access token from an Auth0 authorization server.

flow client credentials auth0

5. OAuth 2.0

To provide authentication, sign in to your Auth0 account.

5.1. Create an application

auth0 create application

5.2. Obtain client id and client secret

You can obtain the application’s domain, client id, and secret in the Auth0 console.

auth0 clientid clientsecret

5.3. Obtain API audience

Go to Applications → APIs and copy the API Audience:

auth0 applications apis

5.4. Authorize application

In the API Settings, authorize the application in the Machine to Machine Applications tab:

auth0 machine to machine applications

6. Writing the application

6.1. Enable annotation Processing

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

annotationprocessorsintellij

6.2. Dependencies

Modify every application (bookinventory, bookinventory, and bookrecommendation). Add Micronaut JWT and Micronaut OAuth 2.0 dependencies:

build.gradle
implementation("io.micronaut.security:micronaut-security-oauth2")
implementation("io.micronaut.security:micronaut-security-jwt")

6.3. Changes to Book Inventory service

Annotate the Controller’s method with @Secured:

bookinventory/src/main/kotlin/example/micronaut/BooksController.kt
package example.micronaut

import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import java.util.Optional
import javax.validation.constraints.NotBlank
import io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED
import io.micronaut.security.annotation.Secured

@Controller("/books")
open class BooksController {

    @Produces(TEXT_PLAIN)
    @Get("/stock/{isbn}")
    @Secured(IS_AUTHENTICATED) (1)
    open fun stock(@NotBlank isbn: String): Boolean? =
        bookInventoryByIsbn(isbn).map { (_, stock) -> stock > 0 }.orElse(null)

    private fun bookInventoryByIsbn(isbn: String): Optional<BookInventory> {
        if (isbn == "1491950358") {
            return Optional.of(BookInventory(isbn, 4))
        }
        if (isbn == "1680502395") {
            return Optional.of(BookInventory(isbn, 0))
        }
        return Optional.empty()
    }
}
1 Annotate with io.micronaut.security.Secured to configure secured access. The SecurityRule.IS_AUTHENTICATED expression allows only access to authenticated users.

To validate the tokens issued by Auth0, configure Validation with Remote JKWS:

bookinventory/src/main/resources/application.yml
micronaut:
  security:
    token:
      jwt:
        signatures:
          jwks:
            auth0:
              url: '${OAUTH_JWKS:`https://micronautguides.eu.auth0.com/.well-known/jwks.json`}'

You can obtain the JWKS URL in the .well-known/openid-configuration endpoint.

6.4. Changes to Book Catalogue service

Annotate the Controller’s method with @Secured:

bookcatalogue/src/main/kotlin/example/micronaut/BooksController.kt
package example.micronaut

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED
import io.micronaut.security.annotation.Secured

@Controller("/books")
class BooksController {

    @Get
    @Secured(IS_AUTHENTICATED) (1)
    fun index(): List<Book> = listOf(
            Book("1491950358", "Building Microservices"),
            Book("1680502395", "Release It!"),
            Book("0321601912", "Continuous Delivery:"))
}
1 Annotate with io.micronaut.security.Secured to configure secured access. The SecurityRule.IS_AUTHENTICATED expression allows only access to authenticated users.

To validate the tokens issued by Auth0, configure Validation with Remote JKWS:

bookcatalogue/src/main/resources/application.yml
micronaut:
  security:
    token:
      jwt:
        signatures:
          jwks:
            auth0:
              url: '${OAUTH_JWKS:`https://micronautguides.eu.auth0.com/.well-known/jwks.json`}'

You can obtain the JWKS URL in the .well-known/openid-configuration endpoint.

6.5. Changes to Book Recommendations service

6.5.1. Books Controller Security

The GET /books in the booksrecommendation service is open.

Annotate the Controller’s method with @Secured:

bookrecommendation/src/main/kotlin/example/micronaut/BookController.kt
package example.micronaut

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import io.micronaut.security.rules.SecurityRule.IS_ANONYMOUS
import io.micronaut.security.annotation.Secured

@Controller("/books")
class BookController(
        private val bookCatalogueOperations: BookCatalogueOperations,
        private val bookInventoryOperations: BookInventoryOperations) {

    @Get
    @Secured(IS_ANONYMOUS) (1)
    fun index(): Publisher<BookRecommendation> =

        Flux.from(bookCatalogueOperations.findAll())
                .flatMap { b ->
                    Flux.from(bookInventoryOperations.stock(b.isbn))
                            .filter { hasStock -> hasStock }
                            .map { b }
                }.map { (_, name) -> BookRecommendation(name) }
}
1 Annotate with io.micronaut.security.Secured to configure secured access. The SecurityRule.IS_ANONYMOUS expression will allow access without authentication.

6.5.2. Configuration of HTTP Services URLs

Modify application-dev.yml to point the declarative HTTP clients to the other microservices URLs.

bookrecommendation/src/main/resources/application-dev.yml
micronaut:
  http:
    services:
      bookcatalogue:
        url: 'http://localhost:8081'
      bookinventory:
        url: 'http://localhost:8082'

6.6. Configuration

Add the following OAuth2 configuration:

bookrecommendation/src/main/resources/application.yml
micronaut:
  security:
    oauth2:
      clients:
        auth0: (1)
          client-id: '${OAUTH_CLIENT_ID:xxx}' (2)
          client-secret: '${OAUTH_CLIENT_SECRET:yyy}' (3)
          grant-type: 'client_credentials' (4)
          token:
            url: '${OAUTH_TOKEN_URL:`https://micronautguides.eu.auth0.com/oauth/token`}' (5)
            auth-method: 'client_secret_post' (6)
          client-credentials:
            service-id-regex: 'bookcatalogue|bookinventory' (7)
            additional-request-params:
              audience: '${AUTH0_API_IDENTIFIER:`https://micronautguides.eu.auth0.com/api/v2/`}' (8)
1 OAuth 2.0 client name.
2 Client id. See previous screenshot.
3 Client secret. See previous screenshot.
4 Specify GrantType#CLIENT_CREDENTIALS client-credentials as grant type for this OAuth 2.0 client.
5 Specify the token endpoint URL. You can obtain the token endpoint URL in the .well-known/openid-configuration.
6 Specify AuthenticationMethod#CLIENT_SECRET_POST as the authentication method. This means the client id and client secret are specified in the body of the HTTP request sent to the token endpoint.
7 Propagate the access token obtained from Auth0 to requests sent to the services bookinventory and bookcatalogue. This uses the Micronaut Client Credentials HTTP Client Filter.
8 Auth0 requires the API Identifier with an audience key in the token endpoint request for the client credentials flow.

The previous configuration uses several placeholders with default values. You will need to set up OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and OAUTH_TOKEN_URL environment variables in your Auth0 application.

export OAUTH_CLIENT_ID=XXXXXXXXXX
export OAUTH_CLIENT_SECRET=YYYYYYYYYY
export OAUTH_TOKEN_URL=https://micronautguides.eu.auth0.com/oauth/token

7. Running the Application

7.1. Run bookcatalogue microservice

To run the application, execute ./gradlew run.

...
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081

7.2. Run bookinventory microservice

To run the application, execute ./gradlew run.

...
14:31:13.104 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 506ms. Server Running: http://localhost:8082

7.3. Run bookrecommendation microservice

To run the application, execute ./gradlew run.

...
14:31:57.389 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080

You can run a cURL command to test the whole application:

curl http://localhost:8080/books
[{"name":"Building Microservices"}]

8. 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.

8.1. Native image generation

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

Java 11
$ sdk install java 22.0.0.2.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM.
Java 17
$ sdk install java 22.0.0.2.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 Gradle, run:

./gradlew nativeCompile

The native image is created in build/native/nativeCompile directory and can be run with build/native/nativeCompile/application.

It is possible to customize the name of the native image or pass additional parameters to GraalVM:

build.gradle
graalvmNative {
    binaries {
        main {
            imageName.set('mn-graalvm-application') (1)
            buildArgs.add('--verbose') (2)
        }
    }
}
1 The native image name will now be mn-graalvm-application
2 It is possible to pass extra arguments to build the native image

Run the native images and execute a cURL command to test the whole application:

curl http://localhost:8080/books
[{"name":"Building Microservices"}]

9. Next steps

Read Micronaut OAuth 2.0 Documentation to learn more.

10. Help with the Micronaut Framework

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