Client Credentials Flow with Micronaut and Amazon Cognito
Learn how to use Client Credentials Flow between Micronaut microservices with an Authorization Server provided by Amazon Cognito.
Authors: Sergio del Amo
Micronaut Version: 3.9.2
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
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 Amazon Cognito 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.
-
Download and unzip the source
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
- Returns a list of books. It uses a domain consisting of a book name and an ISBN. -
bookinventory
- Exposes an endpoint to check whether a book has sufficient stock to fulfill an order. It uses a domain consisting of a stock level and an ISBN. -
bookrecommendation
- 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:
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 Amazon Cognito
authorization server.
5. OAuth 2.0
To provide authentication, sign in to your AWS account and go to AWS Cognito.
With the Amazon Cognito SDK, you just write a few lines of code to enable your users to sign-up and sign-in to your mobile and web apps.
Standards-based authentication
Amazon Cognito uses common identity management standards including OpenID Connect, OAuth 2.0, and SAML 2.0.
First, create an Amazon Cognito User Pool:
Amazon Cognito User Pools - A directory for all your users
You can quickly create your own directory to sign up and sign in users, and to store user profiles using Amazon Cognito User Pools. User Pools provide a user interface you can customize to match your application. User Pools also enable easy integration with social identity providers such as Facebook, Google, and Amazon, and enterprise identity providers such as Microsoft Active Directory through SAML.
Amazon Cognito Pools are regional. Thus, annotate the region you are using.
While you setup the Amazon Cognito User Pool, you will need to save the pool id:
Save the client id and client secret:
5.1. Add a domain
5.2. Using the AWS CLI
Some of the following commands use jq
jq is a lightweight and flexible command-line JSON processor
Alternatively, you can use the AWS CLI to create the user pool and client (change the region to your desired value):
export COGNITO_REGION=eu-west-1
export COGNITO_POOL_ID=$(aws cognito-idp create-user-pool --pool-name "Micronaut Guides" --username-attributes "email" --auto-verified-attributes "email" | jq -r '.UserPool.Id')
export OAUTH_CLIENT_ID=$(aws cognito-idp create-user-pool-client --generate-secret --user-pool-id $COGNITO_POOL_ID --client-name "AWS Cognito Micronaut Tutorial" --callback-urls "http://localhost:8080/oauth/callback/cognito" --logout-urls "http://localhost:8080/logout" --supported-identity-providers COGNITO --allowed-o-auth-flows "code" --allowed-o-auth-scopes "phone" "email" "openid" "profile" "aws.cognito.signin.user.admin" --allowed-o-auth-flows-user-pool-client | jq -r '.UserPoolClient.ClientId')
export OAUTH_CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client --user-pool-id $COGNITO_POOL_ID --client-id $OAUTH_CLIENT_ID | jq -r '.UserPoolClient.ClientSecret')
export COGNITO_DOMAIN="micronaut-guides-$RANDOM"
aws cognito-idp create-user-pool-domain --domain $COGNITO_DOMAIN --user-pool-id $COGNITO_POOL_ID
5.3. Resource server
In the App Client Settings
, define Client Credentials
as the selected OAuth 2.0 flow and check the custom scope you created in the previous step:
6. Writing the Application
6.1. Dependencies
Modify every application (bookinventory
, bookinventory
, and bookrecommendation
). Add Micronaut JWT and Micronaut OAuth 2.0 dependencies:
implementation("io.micronaut.security:micronaut-security-oauth2")
implementation("io.micronaut.security:micronaut-security-jwt")
6.2. Changes to Book Inventory service
Annotate the Controller’s method with @Secured
:
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import javax.validation.constraints.NotBlank;
import java.util.Optional;
import static io.micronaut.http.MediaType.TEXT_PLAIN;
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED;
import io.micronaut.security.annotation.Secured;
@Controller("/books")
public class BooksController {
@Produces(TEXT_PLAIN)
@Get("/stock/{isbn}")
@Secured(IS_AUTHENTICATED) (1)
public Boolean stock(@NotBlank String isbn) {
return bookInventoryByIsbn(isbn).map(bi -> bi.getStock() > 0).orElse(null);
}
private Optional<BookInventory> bookInventoryByIsbn(String isbn) {
if (isbn.equals("1491950358")) {
return Optional.of(new BookInventory(isbn, 4));
}
if (isbn.equals("1680502395")) {
return Optional.of(new 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 Amazon Cognito, configure Validation with Remote JKWS:
micronaut:
security:
token:
jwt:
signatures:
jwks:
auth0:
url: 'https://cognito-idp.${COGNITO_REGION:us-east-1}.amazonaws.com/${COGNITO_POOL_ID:us-east-1_blablabla}/.well-known/jwks.json'
You can obtain the JWKS URL in the https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_POOL_ID}/.well-known/openid-configuration
endpoint.
6.3. Changes to Book Catalogue service
Annotate the Controller’s method with @Secured
:
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED;
import java.util.Arrays;
import java.util.List;
@Controller("/books")
public class BooksController {
@Get
@Secured(IS_AUTHENTICATED) (1)
public List<Book> index() {
return Arrays.asList(
new Book("1491950358", "Building Microservices"),
new Book("1680502395", "Release It!"),
new 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 Cognito, configure Validation with Remote JKWS:
micronaut:
security:
token:
jwt:
signatures:
jwks:
auth0:
url: 'https://cognito-idp.${COGNITO_REGION:us-east-1}.amazonaws.com/${COGNITO_POOL_ID:us-east-1_blablabla}/.well-known/jwks.json'
You can obtain the JWKS URL in the https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_POOL_ID}/.well-known/openid-configuration
endpoint.
6.4. Changes to Book Recommendations service
6.4.1. Books Controller security
The GET /books
in the booksrecommendation
service is open.
Annotate the Controller’s method with @Secured
:
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 static io.micronaut.security.rules.SecurityRule.IS_ANONYMOUS;
import io.micronaut.security.annotation.Secured;
@Controller("/books")
public class BookController {
private final BookCatalogueOperations bookCatalogueOperations;
private final BookInventoryOperations bookInventoryOperations;
public BookController(BookCatalogueOperations bookCatalogueOperations,
BookInventoryOperations bookInventoryOperations) {
this.bookCatalogueOperations = bookCatalogueOperations;
this.bookInventoryOperations = bookInventoryOperations;
}
@Get
@Secured(IS_ANONYMOUS) (1)
public Publisher<BookRecommendation> index() {
return Flux.from(bookCatalogueOperations.findAll())
.flatMap(b -> Flux.from(bookInventoryOperations.stock(b.getIsbn()))
.filter(Boolean::booleanValue)
.map(rsp -> b)
).map(book -> new BookRecommendation(book.getName()));
}
}
1 | Annotate with io.micronaut.security.Secured to configure secured access. The SecurityRule.IS_ANONYMOUS expression will allow access without authentication. |
6.4.2. Configuration of HTTP services URLs
Modify application-dev.yml
to point the declarative HTTP clients to the other microservices URLs.
micronaut:
http:
services:
bookcatalogue:
url: 'http://localhost:8081'
bookinventory:
url: 'http://localhost:8082'
6.5. Configuration
Add the following OAuth2 configuration:
micronaut:
security:
oauth2:
clients:
cognito: (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://micronautguide.auth.us-east-1.amazoncognito.com/oauth2/token`}' (5)
auth-method: client_secret_basic (6)
client-credentials:
service-id-regex: 'bookcatalogue|bookinventory' (7)
scope: 'example.micronaut.books/books.read' (8)
1 | OAuth 2.0 Client name. |
2 | Client ID. See previous screenshot. |
3 | Client Secret. See previous screenshot. |
4 | Specify GrantType#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 https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_POOL_ID}/.well-known/openid-configuration OpenID auto-configuration endpoint. |
6 | Specify AuthenticationMethod#CLIENT_SECRET_BASIC as the authentication method. This means that basic authentication with client id as username and client secret as password is used for the HTTP request sent to the token endpoint. |
7 | Propagate the access token obtained from Amazon Cognito to requests sent to the services bookinventory and bookcatalogue . This uses the Micronaut Client Credentials HTTP Client Filter. |
8 | Request the custom scope that you set in Amazon Cognito Resource Groups. |
The previous configuration uses several placeholders with default values. You will need to set up your Amazon Cognito application’s OAUTH_CLIENT_ID
, OAUTH_CLIENT_SECRET
, and OAUTH_TOKEN_URL
environment variables.
7. Running the Application
7.1. Run bookcatalogue
microservice
export COGNITO_REGION=us-east-1
export COGNITO_POOL_ID=us-east-1_blablabla
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
export COGNITO_REGION=us-east-1
export COGNITO_POOL_ID=us-east-1_blablabla
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
export OAUTH_CLIENT_ID=XXXXXXXXXX
export OAUTH_CLIENT_SECRET=YYYYYYYYYY
export OAUTH_TOKEN_URL=https://micronautguide.auth.us-east-1.amazoncognito.com/oauth2/token
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 Executable with GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.
Compiling native executables 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 executable generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.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 executable using Gradle, run:
./gradlew nativeCompile
The native executable is created in build/native/nativeCompile
directory and can be run with build/native/nativeCompile/micronautguide
.
It is possible to customize the name of the native executable or pass additional parameters to GraalVM:
graalvmNative {
binaries {
main {
imageName.set('mn-graalvm-application') (1)
buildArgs.add('--verbose') (2)
}
}
}
1 | The native executable name will now be mn-graalvm-application |
2 | It is possible to pass extra arguments to build the native executable |
Run the native executables 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
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.