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.
The article uses and covers:
- 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
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 theBasicAuthRequestInterceptor
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.
- Add WireMock dependecy in
build.gradle
testImplementation ‘com.github.tomakehurst:wiremock:2.27.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
Turn on full logging of OpenFeign clients
@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.
401 Unauthorized
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
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.