Spring Boot Using Multiple OpenFeign Clients To Authenticate REST API Calls

Spring Boot application that is using OpenFeign client to connect to a 3rd party REST API is a well-known approach. But what if you need to call multiple APIs (like Jira, Slack, Google, …) with different users and authentication methods? This article shows how to set up Basic Authentication for Jira (Zephyr API in particular) and how to use a token for Slack API at the same time.

Photo by Kit Suman on Unsplash
  • Spring Boot Application (with one REST controller to process a request)
  • Configuration of OpenFeign client for Jira API (Basic Auth)
  • Configuration of OpenFeign client for Slack API (Bearer Token Auth)
  • Integration test verifying the correct Authorization headers (WireMock)

For reference see the whole sample project on GitHub.

Spring Boot Application

Start with Spring Initializr and add the necessary dependencies:

  • Spring Web — our application itself is a REST API with one endpoint
  • OpenFeign — to call Jira and Slack APIs
  • Lombok — for some handy getters and setters in DTO classes
Spring Initializr Dependencies

Configure OpenFeign Client for Jira API

In this section, we connect to Zephyr part of the Jira API. Let’s use Get list of executions endpoint to showcase the functionality.

Define your zephyr.api.baseUrl in your application.yml like:

zephyr:
api:
baseUrl: https://jira.my-company.com/jira/rest/zapi/latest

Zephyr API uses basic authentication. To make your Feign Client provide the correct authorization header, use a configuration class where you define a bean of BasicAuthRequestInterceptor .

Notice — do not mark the whole class as @Configuration . This would make the BasicAuthRequestInterceptor bean available in the whole Spring context and therefore it would be picked up by other Feign Clients. That would interact with the Slack Feign Client that we define next.

In this article’s sample GitHub project, you can use JIRA_API_USERNAME and JIRA_API_PASSWORD environment variables to provide the credentials. (Notice — your Jira username is usually not your email.)

Configure OpenFeign Client for Slack API

As a second OpenFeign client let’s configure a client that is sending messages to Slack API.

The Slack API base URL is https://slack.com/api and it uses token authentication. You can find your token in a Slack application that you need to create to start using the API. Token is visible in the configuration page of your application at https://api.slack.com/apps/YOUAR_APP_ID.

Use configuration to define a RequestInterceptor that adds Authorization header with Bearer token to all Slack requests.

In this article’s sample GitHub project, you can use SLACK_AUTH_TOKEN environment variable to provide the token.

Controller and Service

Let’s just create a simple controller with GET endpoint at /api/v1/resource that will trigger a service that calls both Jira and Slack APIs.

If all environment variables are correctly set up and correct credentials are provided, this should work — but let’s test it also in an integration test.

WireMock Integration Test verifying Authorization Headers

When creating 3rd party API calls in our application, we can verify that proper endpoints with a proper request structure were called using WireMock. Of course, we also mock responses from those APIs to verify our code (and business process) reacts accordingly.

Let’s create a JUnit 5 integration test using @SprinBootTest — see the complete integration test on GitHub.

  1. Add WireMock dependecy in build.gradle
    testImplementation ‘com.github.tomakehurst:wiremock:2.27.2’
  2. With JUnit 5 we cannot use WireMock through @Rule , but we have to use an extension.

This enables us to create multiple WireMock servers on random ports during the @BeforeAll.

@BeforeAll
public static void beforeAll(@WireMockInstance WireMockServer server1, @WireMockInstance WireMockServer server2) {
zephyrWiremockServer = server1;
slackWiremockServer = server2;
}

3. Because WireMock servers start on random ports, we need to set up our Jira and Slack API base URLs on the fly. This is done using initializers in @ContextConfiguration.

4. Define fake credentials used in your tests.

@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"zephyr.api.username=zephyrTestUser",
"zephyr.api.password=zephyrTestPassword",
"slack.app.oauth.accessToken=testFakeToken"
})

5. Verify that all requests to Jira and Slack APIs had the proper Authorization headers in @AfterEach.

zephyrWiremockServer.verify(anyRequestedFor(anyUrl())
.withBasicAuth(new BasicCredentials("zephyrTestUser", "zephyrTestPassword")));
slackWiremockServer.verify(anyRequestedFor(anyUrl())
.withHeader("Authorization", equalTo("Bearer testFakeToken")));

6. You should also verify that there was exactly one Authozitation header in your requests. There could be potentially multiple Authozitation headers and WireMock would match any of them. To assert that there is only one header that you expect is a bit tricky with WireMock, but can be achieved using addMockServiceRequestListener in @BeforeAll and later asserting it in @AfterEach.

7. Finally, create a test method where you call the controller, mock the Jira, and Slack API responses, and verify the response of your controller.
https://github.com/zpapez/spring-feign-clients/blob/707dd17fd5cc3a93a7ffa031a8fa1de405e19c8c/src/test/java/cz/zpapez/springfeignclients/IntegrationTest.java#L102

Troubleshooting

@Configuration
public class MyConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

and in properties

logging.level.cz.zpapez.springfeignclients.zephyr.ZephyrFeignClient: DEBUG
logging.level.cz.zpapez.springfeignclients.slack.SlackFeignClient: DEBUG

This setup prints all requests including headers and also responses that are being sent. You can verify proper values in proper authorization headers by reading these logs.

Double-check that you provide the correct credentials (like Jira username, not email) in the environment variables. Maybe also try to make the same request work usingcurl.

No default constructor found when running the @SpringBootTest with context configuration initializers @ContextConfiguration(initializers = AppIntegrationTestPropertyInitializer.class).

Caused by: org.springframework.beans.BeanInstantiationException:
Failed to instantiate [cz.zpapez.springfeignclients.IntegrationTest$AppIntegrationTestPropertyInitializer]: No default constructor found; nested exception is java.lang.NoSuchMethodException:
cz.zpapez.springfeignclients.IntegrationTest$AppIntegrationTestPropertyInitializer.<init>()

The class specified as the initializer (implementing ApplicationContextInitializer) cannot be an inner class of the integration test class. Make sure that you define the property initializer class after the clossing } of the integration test class.

Software engineer and active sportsman