OAuth2 in Spring Boot Native/Reactive microservice, with Amazon Cognito as OIDC service.

Andres Solorzano
8 min readMay 21, 2023

--

In our previous tutorial, we built a Spring Native microservice using “Bean Validation” to validate Java objects and return i18n messages if the validation fails. Now, it’s time to deploy an Amazon Cognito service as an OIDC server to authenticate the users of the City Tasks application. This tutorial is a conjunction of previous tutorials I wrote about JWT Validation using the Quarkus Framework and the configuration of Cognito as an Identity Provider (IdP) to deliver SSO credentials. As we advance with the development of this project, it’s time to accommodate some of the previous technologies we used and integrate them into our City Tasks application. So, let’s start.

To complete this guide, you’ll need the following tools:

NOTE: You can download the source code of the Task Service with all the configurations we talk about in this article from my GitHub repository.

Configuring OAuth2 Resource Server.

Add the following Maven dependency to indicate to Spring Boot that our microservice will be acting as an “OAuth2 Resource Server”:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Remember that Spring Boot uses auto-configuration based on project dependencies. So, behind the scenes, Spring Boot creates a web filter chain bean with the following default configuration for Reactive microservices:

@Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}

The last thing that we need to specify is the “Auth Server URI” in the application properties file:

spring.security.oauth2.resourceserver.jwt.issuer-uri=<your_oidc_server_uri>

But let’s discuss Integration Tests in the next section (as we use TDD) before configuring the OIDC server.

TDD with Integration Tests and Testcontainers.

We’ve been using Testcontainers for a while to execute our Integration Tests. For DynamoDB, we used the Localstack, which has many AWS services internally to deploy locally for development and testing purposes. But Localstack doesn’t have Cognito as an internal service, so I used KeyCloak as the OIDC service to execute the integration tests:

<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>2.1.2</version>
<scope>test</scope>
</dependency>

Now we can add the Keycloak container definition to our test container’s base class:

Notice that we specified a “keycloak-realm.json” file, which contains properties of a standard configuration of a Keycloak service. You can review this file in the source code if you are interested in these properties in more detail.

If we try to run the integration tests, we’ll receive an HTTP 401 error in the controller classes because we don’t specify a valid JWT token in the HTTP request headers:

So, we need to add the following methods in the test container’s base class:

Now, we must add the “Authorization” header in all the HTTP requests of the controller test classes like this:

Let’s try to execute the integration tests one more time to see the results:

All our integration tests are running successfully using KeyCloak as an OIDC server. In the next section, it’s time to configure our Cognito service as an OIDC server for our application.

IAM Identity Center (Optional).

I configured an IAM Identity Center for a Multi-Account environment on AWS in my previous tutorial. In that article, I used my IdP-Pre account to deploy the last OIDC service. So you can use that login script to access any of your Organization’s accounts:

$ hiperium-login

Now, we can deploy our Cognito OIDC service using the Amplify CLI.

Amazon Cognito as Identity Provider.

Our previous tutorial configured a Cognito service as an Identity Provider (IdP) for the Tasks Service built-in Quarkus. We need a new Cognito OIDC service for the latest version of our Task Service built-in Spring Boot. So execute the following command inside a new empty directory:

$ amplify init

Remember that many of these configurations we made in my previous tutorial. So you can read it for more details.

Then, add the Authentication support for Cognito:

$ amplify add auth

IMPORTANT: As we don’t have the Angular app now, we’ll use the Postman tool to make tests. So notice in the image below that I used the URL “https://oauth.pstmn.io/v1/browser-callback/” in the OAuth Flow settings:

Before deploying the Cognito configuration, let’s discuss the OIDC application client’s secrets configurations. By default, Cognito generates 2 application clients with an empty secret for security reasons. This is thinking in web or mobile apps using the “Proof Key for Code Exchange” (PKCE) with the Auth Code Flow. But for our back-end service, we need this app client credentials to access the Task Service endpoints. So, we need to update the previous configuration manually to take effect.

IMPORTANT: Go to the “amplify/backend/auth/CityIdP” folder and open the “parameters.json” and the “cli-inputs.json” files. Find the “userpoolClientGenerateSecret” property in both files and change its value to “true”:

"aliasAttributes": [],
"userpoolClientGenerateSecret": true,
"userpoolClientRefreshTokenValidity": 30,

Now, you can deploy your custom Cognito configurations into AWS:

$ amplify push

At the end of the command execution, you must see a message like this:

Our Cognito Domain is the following:

https://city-idp-pre-pre.auth.us-east-1.amazoncognito.com

We also need the “Cognito User Pool ID” to complete the Auth Server URI. So go to the AWS console, select the cognito service, and copy the required parameter:

So our Auth Server URI is:

https://cognito-idp.us-east-1.amazonaws.com/us-east-1_J2eeVf4tx

Following the OIDC convention, we can obtain the OIDC configuration by adding the “/.well-known/openid-configuration” path to our Server Auth URI. So open the Postman tool and execute a GET request:

As our Cognito OIDC service is working, we can now configure our Task Service to use the Auth Server URI and make some tests.

Deploying using Docker Compose.

Go to the City Tasks project directory and open the “docker-compose.yml” file. Find the following environment variable parameter and set the correct Auth Server URI value:

environment:
- CITY_IDP_ENDPOINT=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_J2eeVf4tx

Then, start the docker cluster in the terminal window:

$ docker compose up --build

The PostgreSQL, DynamoDB, and Task Service containers must be deployed without any problem:

Now it’s time to continue with the service tests but using Postman.

Task Service Endpoints Tests.

Let’s try to access the current Tasks using Postman. I created a file in the resources folder called “data.sql,” which loads 4 Tasks at deployment time for testing purposes.

As we expected, we got an HTTP 401 error because we were not logged into the Cognito IdP service. So, we need a valid JWT token to access our Task Service data. First, we need the app client ID and secret from the Cognito console at the end of the “App Integration” tab:

As I mentioned before, we need to use the first one, which Cognito generates a client secret. So click on the first app client link, and then enable the “Show client secret” option to see the generated secret:

Also, we need to create a user for our tests. As we recently created this User Pool, we don’t have any users:

Click the “Create user” button to fill in the required information. After that, your user will be created:

Then, return to the Postman tool, open the “Authorization” tab, and select the “OAuth 2.0” option in the “Type” box. In the “Configure New Token” enter the required information:

The “Auth URL” parameter corresponds to the “authorization_endpoint” in the OIDC configuration. The “Access Token URL” also corresponds to the “token_endpoint” in the OIDC configuration. The “Client ID” and “Client Secret” parameters correspond to the ones defined in the “App Client Information” section in the Cognito console.

After entering these values, click the “Get New Access Token” button. Postman will open a little window that contains the Cognito-hosted UI to enter the client credentials for the user you created before:

As this is a new user, Cognito forces us to update the password if this is the first login:

IMPORTANT: The Cognito OIDC service redirects our application to the defined URI when we use the Amplify command in the previous section:

https://oauth.pstmn.io/v1/browser-callback/

The Postman tool manages this URI, so the tool can obtain the Authorization Code first and, in a second request, will ask for the Access Token:

Now, Postman stores this Access Token for our Task Service endpoint executions. So now, we can try to perform the GET operation one more time to obtain our Tasks objects:

As you can see, we get the 4 Tasks objects defined in our “data.sql” file. Let’s open a new Postman tab that calls a search function of the Tasks. In this user case, let’s find all tasks that the device operation is for activation:

And that’s it!!! Now we have an OIDC server deployed on AWS using the Cognito service, and our Task Service acting as an OAuth2 Resource Server, which interacts with the Auth Service (Cognito) to validate the JWT token added in every HTTP requests made by a logged user.

In my following tutorial, we’re deploying our Task Service in AWS using the Amazon Copilot CLI, as we did with the Quarkus version of this service. So, our Task Service will be deployed in the ECS service, and we will configure a CI/CD Pipeline as usual. So stay tuned.

I hope this tutorial was helpful :).

--

--