Table of Contents
Summary
In a multi-threaded environment like an API server, if you want OpenAPI Generator Client to relay access token, you need to create an ApiClient
for each request.
Problem Situation
OpenAPI Specification 3 is a specification for defining HTTP APIs. As it is written in forms like YAML files, it allows the creation of language-agnostic API specifications. OpenAPI Generator is a project to generate client/server code in various languages from documents written in OAS. Below is an example of client code generated by OpenAPI Generator.
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-03-21T20:39:48.722754+09:00[Asia/Seoul]")
public class OrderApi {
private ApiClient apiClient;
public OrderApi() {
this(new ApiClient());
}
@Autowired
public OrderApi(ApiClient apiClient) {
this.apiClient = apiClient;
}
public ApiClient getApiClient() {
return apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.apiClient = apiClient;
}
/**
* Comment
*/
public Flux<Response> apiName(Request req) throws WebClientResponseException {
// Implementation
}
}
Now, there may be situations where you want to relay user access token in order to send request on behalf of the user. From above example, you may want to make an request to Order microservice from a mobile BFF(Backend-For-Frontend). However, the generated code for an api does not have argument for user access token. Then how should one pass something like a user’s JWT token for authentication/authorization?
Authentication/Authorization
After some trial and error, I found out that I have to set the token in the ApiClient object used to call the API in the client implementation. ApiClient has supported bearer tokens since 2019, as seen in the following issue: https://github.com/OpenAPITools/openapi-generator/issues/457
public class ApiClient extends JavaTimeFormatter {
// ...
private Map<String, Authentication> authentications;
// ...
public void setBearerToken(String bearerToken) {
for (Authentication auth : authentications.values()) {
if (auth instanceof HttpBearerAuth) {
((HttpBearerAuth) auth).setBearerToken(bearerToken);
return;
}
}
throw new RuntimeException("No Bearer authentication configured!");
}
// ...
}
Great! Now I just have to set bearer token and use that ApiClient, right? Unfortunately authentications
field is not thread-safe. If you were to create a single instance of ApiClient and reuse it in multiple places, there would be a concurrency issue. This can result in security problem where user can get results of an another user.
Refer to the above image. An OrderApi
object would normally be shared between different requests. This reduces the overhead of having to initialize api object every time. However, there can be a concurrency issue when multiple requests are made at the same time. When User A and User B make requests concurrency, a token set by User A could be replaced by token of User B. When requests are made to Order microservice, token of User B would be passed instead. How should we fix this problem?
Solution to Multi-Threaded Environment and OpenAPI Generator Client
The solution is to create new instance of ApiClient
for each request. OpenAPI Generator-generated Api classes provide the following constructors:
public OrderApi() {
this(new ApiClient());
}
@Autowired
public OrderApi(ApiClient apiClient) {
this.apiClient = apiClient;
}
public ApiClient getApiClient() {
return apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.apiClient = apiClient;
}
The heaviest operation of initializing an ApiClient
object is creating the underlying web request object. In our case this is a WebClient instance from Spring Reactive stack. ApiClient has a constructor that receives a WebClient object. So by only sharing WebClient instance and initializing ApiClient for each request, you can avoid concurrent token problem with minimal performance impact.
public ApiClient(WebClient webClient, ObjectMapper mapper, DateFormat format) {
this(Optional.ofNullable(webClient).orElseGet(() ->buildWebClient(mapper.copy())), format);
}
private ApiClient(WebClient webClient, DateFormat format) {
this.webClient = webClient;
this.dateFormat = format;
this.init();
}
protected void init() {
// Setup authentications (key: authentication name, value: authentication).
authentications = new HashMap<String, Authentication>();
authentications.put("bearer", new HttpBearerAuth("bearer"));
// Prevent the authentications from being modified.
authentications = Collections.unmodifiableMap(authentications);
}
// Spring Application Example
@Service
class OrderService(private val webClient: WebClient) {
fun makeRequest(token: String) {
val apiClient = ApiClient(webClient, //some DateFormat)
val orderApi = OrderApi(apiClient)
orderApi.makeRequet()
}
}
And this is actually the recommended way of using Api objects generated by OpenAPI Generator as per README.md from the sample of official Github Repository explains.
Recommendation
It’s recommended to create an instance of
https://github.com/OpenAPITools/openapi-generator/blob/v7.4.0/samples/client/petstore/java/webclient/README.md#recommendationApiClient
per thread in a multithreaded environment to avoid any potential issues.