diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 49985d8e1ecd..20b707283b14 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -604,6 +604,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen if (FEIGN.equals(getLibrary())) { supportingFiles.add(new SupportingFile("auth/OauthPasswordGrant.mustache", authFolder, "OauthPasswordGrant.java")); supportingFiles.add(new SupportingFile("auth/OauthClientCredentialsGrant.mustache", authFolder, "OauthClientCredentialsGrant.java")); + supportingFiles.add(new SupportingFile("auth/ApiErrorDecoder.mustache", authFolder, "ApiErrorDecoder.java")); } } } diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache index 20c8733c9d08..3166dda84324 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache @@ -32,9 +32,18 @@ import feign.form.FormEncoder; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import feign.slf4j.Slf4jLogger; -import {{invokerPackage}}.auth.*; +import {{invokerPackage}}.auth.HttpBasicAuth; +import {{invokerPackage}}.auth.HttpBearerAuth; +import {{invokerPackage}}.auth.ApiKeyAuth; + {{#hasOAuthMethods}} +import {{invokerPackage}}.auth.ApiErrorDecoder; +import {{invokerPackage}}.auth.OAuth; import {{invokerPackage}}.auth.OAuth.AccessTokenListener; +import {{invokerPackage}}.auth.OAuthFlow; +import {{invokerPackage}}.auth.OauthPasswordGrant; +import {{invokerPackage}}.auth.OauthClientCredentialsGrant; +import feign.Retryer; {{/hasOAuthMethods}} {{>generatedAnnotation}} @@ -55,6 +64,10 @@ public class ApiClient { .client(new OkHttpClient()) .encoder(new FormEncoder(new JacksonEncoder(objectMapper))) .decoder(new JacksonDecoder(objectMapper)) + {{#hasOAuthMethods}} + .errorDecoder(new ApiErrorDecoder()) + .retryer(new Retryer.Default(0, 0, 2)) + {{/hasOAuthMethods}} .logger(new Slf4jLogger()); } diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/ApiErrorDecoder.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/ApiErrorDecoder.mustache new file mode 100644 index 000000000000..d3587925abfd --- /dev/null +++ b/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/ApiErrorDecoder.mustache @@ -0,0 +1,25 @@ +package {{invokerPackage}}.auth; + +import feign.Response; +import feign.RetryableException; +import feign.codec.ErrorDecoder; + +/** + * Error decoder that makes the HTTP 401 and 403 Retryable. Sometimes the 401 or 402 may indicate an expired token + * All the other HTTP status are handled by the {@link feign.codec.ErrorDecoder.Default} decoder + */ +public class ApiErrorDecoder implements ErrorDecoder { + + private final Default defaultErrorDecoder = new Default(); + + @Override + public Exception decode(String methodKey, Response response) { + //401/403 response codes most likely indicate an expired access token, unless it happens two times in a row + Exception httpException = defaultErrorDecoder.decode(methodKey, response); + if (response.status() == 401 || response.status() == 403) { + return new RetryableException(response.status(), "Received status " + response.status() + " trying to renew access token", + response.request().httpMethod(), httpException, null, response.request()); + } + return httpException; + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/OAuth.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/OAuth.mustache index 864ee3fe3a5c..e6082965643a 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/OAuth.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/feign/auth/OAuth.mustache @@ -2,10 +2,10 @@ package {{invokerPackage}}.auth; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.oauth.OAuth20Service; -import feign.Request.HttpMethod; import feign.RequestInterceptor; import feign.RequestTemplate; -import feign.RetryableException; + +import java.util.Collection; {{>generatedAnnotation}} public abstract class OAuth implements RequestInterceptor { @@ -34,25 +34,27 @@ public abstract class OAuth implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // If the request already have an authorization (eg. Basic auth), do nothing - if (template.headers().containsKey("Authorization")) { + if (requestContainsNonOauthAuthorization(template)) { return; } - // If first time, get the token - if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) { - updateAccessToken(template); - } - if (getAccessToken() != null) { - template.header("Authorization", "Bearer " + getAccessToken()); + String accessToken = getAccessToken(); + if (accessToken != null) { + template.header("Authorization", "Bearer " + accessToken); } } - private synchronized void updateAccessToken(RequestTemplate template) { - OAuth2AccessToken accessTokenResponse; - try { - accessTokenResponse = getOAuth2AccessToken(); - } catch (Exception e) { - throw new RetryableException(0, e.getMessage(), HttpMethod.POST, e, null, template.request()); + private boolean requestContainsNonOauthAuthorization(RequestTemplate template) { + Collection authorizations = template.headers().get("Authorization"); + if (authorizations == null) { + return false; } + return !authorizations.stream() + .anyMatch(authHeader -> !authHeader.equalsIgnoreCase("Bearer")); + } + + private synchronized void updateAccessToken() { + OAuth2AccessToken accessTokenResponse; + accessTokenResponse = getOAuth2AccessToken(); if (accessTokenResponse != null && accessTokenResponse.getAccessToken() != null) { setAccessToken(accessTokenResponse.getAccessToken(), accessTokenResponse.getExpiresIn()); if (accessTokenListener != null) { @@ -70,9 +72,18 @@ public abstract class OAuth implements RequestInterceptor { } public synchronized String getAccessToken() { + // If first time, get the token + if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) { + updateAccessToken(); + } return accessToken; } + /** + * Manually sets the access token + * @param accessToken The access token + * @param expiresIn Seconds until the token expires + */ public synchronized void setAccessToken(String accessToken, Integer expiresIn) { this.accessToken = accessToken; this.expirationTimeMillis = expiresIn == null ? null : System.currentTimeMillis() + expiresIn * MILLIS_PER_SECOND; diff --git a/samples/client/petstore/java/feign-no-nullable/.openapi-generator/FILES b/samples/client/petstore/java/feign-no-nullable/.openapi-generator/FILES index cd394fffa07e..b26b7d343061 100644 --- a/samples/client/petstore/java/feign-no-nullable/.openapi-generator/FILES +++ b/samples/client/petstore/java/feign-no-nullable/.openapi-generator/FILES @@ -27,6 +27,7 @@ src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java src/main/java/org/openapitools/client/api/PetApi.java src/main/java/org/openapitools/client/api/StoreApi.java src/main/java/org/openapitools/client/api/UserApi.java +src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java src/main/java/org/openapitools/client/auth/ApiKeyAuth.java src/main/java/org/openapitools/client/auth/DefaultApi20Impl.java src/main/java/org/openapitools/client/auth/HttpBasicAuth.java diff --git a/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/ApiClient.java index 674fa2c45a40..0bbcbbbb455d 100644 --- a/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/ApiClient.java @@ -19,8 +19,17 @@ import feign.form.FormEncoder; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import feign.slf4j.Slf4jLogger; -import org.openapitools.client.auth.*; +import org.openapitools.client.auth.HttpBasicAuth; +import org.openapitools.client.auth.HttpBearerAuth; +import org.openapitools.client.auth.ApiKeyAuth; + +import org.openapitools.client.auth.ApiErrorDecoder; +import org.openapitools.client.auth.OAuth; import org.openapitools.client.auth.OAuth.AccessTokenListener; +import org.openapitools.client.auth.OAuthFlow; +import org.openapitools.client.auth.OauthPasswordGrant; +import org.openapitools.client.auth.OauthClientCredentialsGrant; +import feign.Retryer; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen") public class ApiClient { @@ -40,6 +49,8 @@ public class ApiClient { .client(new OkHttpClient()) .encoder(new FormEncoder(new JacksonEncoder(objectMapper))) .decoder(new JacksonDecoder(objectMapper)) + .errorDecoder(new ApiErrorDecoder()) + .retryer(new Retryer.Default(0, 0, 2)) .logger(new Slf4jLogger()); } diff --git a/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java b/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java new file mode 100644 index 000000000000..4ddfc678e92f --- /dev/null +++ b/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java @@ -0,0 +1,25 @@ +package org.openapitools.client.auth; + +import feign.Response; +import feign.RetryableException; +import feign.codec.ErrorDecoder; + +/** + * Error decoder that makes the HTTP 401 and 403 Retryable. Sometimes the 401 or 402 may indicate an expired token + * All the other HTTP status are handled by the {@link feign.codec.ErrorDecoder.Default} decoder + */ +public class ApiErrorDecoder implements ErrorDecoder { + + private final Default defaultErrorDecoder = new Default(); + + @Override + public Exception decode(String methodKey, Response response) { + //401/403 response codes most likely indicate an expired access token, unless it happens two times in a row + Exception httpException = defaultErrorDecoder.decode(methodKey, response); + if (response.status() == 401 || response.status() == 403) { + return new RetryableException(response.status(), "Received status " + response.status() + " trying to renew access token", + response.request().httpMethod(), httpException, null, response.request()); + } + return httpException; + } +} \ No newline at end of file diff --git a/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/OAuth.java b/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/OAuth.java index d41eca7a0a53..0ae73ee19c8b 100644 --- a/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/OAuth.java +++ b/samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/auth/OAuth.java @@ -2,10 +2,10 @@ package org.openapitools.client.auth; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.oauth.OAuth20Service; -import feign.Request.HttpMethod; import feign.RequestInterceptor; import feign.RequestTemplate; -import feign.RetryableException; + +import java.util.Collection; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen") public abstract class OAuth implements RequestInterceptor { @@ -34,25 +34,27 @@ public abstract class OAuth implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // If the request already have an authorization (eg. Basic auth), do nothing - if (template.headers().containsKey("Authorization")) { + if (requestContainsNonOauthAuthorization(template)) { return; } - // If first time, get the token - if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) { - updateAccessToken(template); - } - if (getAccessToken() != null) { - template.header("Authorization", "Bearer " + getAccessToken()); + String accessToken = getAccessToken(); + if (accessToken != null) { + template.header("Authorization", "Bearer " + accessToken); } } - private synchronized void updateAccessToken(RequestTemplate template) { - OAuth2AccessToken accessTokenResponse; - try { - accessTokenResponse = getOAuth2AccessToken(); - } catch (Exception e) { - throw new RetryableException(0, e.getMessage(), HttpMethod.POST, e, null, template.request()); + private boolean requestContainsNonOauthAuthorization(RequestTemplate template) { + Collection authorizations = template.headers().get("Authorization"); + if (authorizations == null) { + return false; } + return !authorizations.stream() + .anyMatch(authHeader -> !authHeader.equalsIgnoreCase("Bearer")); + } + + private synchronized void updateAccessToken() { + OAuth2AccessToken accessTokenResponse; + accessTokenResponse = getOAuth2AccessToken(); if (accessTokenResponse != null && accessTokenResponse.getAccessToken() != null) { setAccessToken(accessTokenResponse.getAccessToken(), accessTokenResponse.getExpiresIn()); if (accessTokenListener != null) { @@ -70,9 +72,18 @@ public abstract class OAuth implements RequestInterceptor { } public synchronized String getAccessToken() { + // If first time, get the token + if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) { + updateAccessToken(); + } return accessToken; } + /** + * Manually sets the access token + * @param accessToken The access token + * @param expiresIn Seconds until the token expires + */ public synchronized void setAccessToken(String accessToken, Integer expiresIn) { this.accessToken = accessToken; this.expirationTimeMillis = expiresIn == null ? null : System.currentTimeMillis() + expiresIn * MILLIS_PER_SECOND; diff --git a/samples/client/petstore/java/feign/.openapi-generator/FILES b/samples/client/petstore/java/feign/.openapi-generator/FILES index 628c2dae9f91..81350a81f2d5 100644 --- a/samples/client/petstore/java/feign/.openapi-generator/FILES +++ b/samples/client/petstore/java/feign/.openapi-generator/FILES @@ -28,6 +28,7 @@ src/main/java/org/openapitools/client/api/FakeClassnameTags123Api.java src/main/java/org/openapitools/client/api/PetApi.java src/main/java/org/openapitools/client/api/StoreApi.java src/main/java/org/openapitools/client/api/UserApi.java +src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java src/main/java/org/openapitools/client/auth/ApiKeyAuth.java src/main/java/org/openapitools/client/auth/DefaultApi20Impl.java src/main/java/org/openapitools/client/auth/HttpBasicAuth.java diff --git a/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/ApiClient.java b/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/ApiClient.java index b9b1d341056a..151e595c51b6 100644 --- a/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/ApiClient.java +++ b/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/ApiClient.java @@ -20,8 +20,17 @@ import feign.form.FormEncoder; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import feign.slf4j.Slf4jLogger; -import org.openapitools.client.auth.*; +import org.openapitools.client.auth.HttpBasicAuth; +import org.openapitools.client.auth.HttpBearerAuth; +import org.openapitools.client.auth.ApiKeyAuth; + +import org.openapitools.client.auth.ApiErrorDecoder; +import org.openapitools.client.auth.OAuth; import org.openapitools.client.auth.OAuth.AccessTokenListener; +import org.openapitools.client.auth.OAuthFlow; +import org.openapitools.client.auth.OauthPasswordGrant; +import org.openapitools.client.auth.OauthClientCredentialsGrant; +import feign.Retryer; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen") public class ApiClient { @@ -41,6 +50,8 @@ public class ApiClient { .client(new OkHttpClient()) .encoder(new FormEncoder(new JacksonEncoder(objectMapper))) .decoder(new JacksonDecoder(objectMapper)) + .errorDecoder(new ApiErrorDecoder()) + .retryer(new Retryer.Default(0, 0, 2)) .logger(new Slf4jLogger()); } diff --git a/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java b/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java new file mode 100644 index 000000000000..4ddfc678e92f --- /dev/null +++ b/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/ApiErrorDecoder.java @@ -0,0 +1,25 @@ +package org.openapitools.client.auth; + +import feign.Response; +import feign.RetryableException; +import feign.codec.ErrorDecoder; + +/** + * Error decoder that makes the HTTP 401 and 403 Retryable. Sometimes the 401 or 402 may indicate an expired token + * All the other HTTP status are handled by the {@link feign.codec.ErrorDecoder.Default} decoder + */ +public class ApiErrorDecoder implements ErrorDecoder { + + private final Default defaultErrorDecoder = new Default(); + + @Override + public Exception decode(String methodKey, Response response) { + //401/403 response codes most likely indicate an expired access token, unless it happens two times in a row + Exception httpException = defaultErrorDecoder.decode(methodKey, response); + if (response.status() == 401 || response.status() == 403) { + return new RetryableException(response.status(), "Received status " + response.status() + " trying to renew access token", + response.request().httpMethod(), httpException, null, response.request()); + } + return httpException; + } +} \ No newline at end of file diff --git a/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/OAuth.java b/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/OAuth.java index d41eca7a0a53..0ae73ee19c8b 100644 --- a/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/OAuth.java +++ b/samples/client/petstore/java/feign/src/main/java/org/openapitools/client/auth/OAuth.java @@ -2,10 +2,10 @@ package org.openapitools.client.auth; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.oauth.OAuth20Service; -import feign.Request.HttpMethod; import feign.RequestInterceptor; import feign.RequestTemplate; -import feign.RetryableException; + +import java.util.Collection; @javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen") public abstract class OAuth implements RequestInterceptor { @@ -34,25 +34,27 @@ public abstract class OAuth implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // If the request already have an authorization (eg. Basic auth), do nothing - if (template.headers().containsKey("Authorization")) { + if (requestContainsNonOauthAuthorization(template)) { return; } - // If first time, get the token - if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) { - updateAccessToken(template); - } - if (getAccessToken() != null) { - template.header("Authorization", "Bearer " + getAccessToken()); + String accessToken = getAccessToken(); + if (accessToken != null) { + template.header("Authorization", "Bearer " + accessToken); } } - private synchronized void updateAccessToken(RequestTemplate template) { - OAuth2AccessToken accessTokenResponse; - try { - accessTokenResponse = getOAuth2AccessToken(); - } catch (Exception e) { - throw new RetryableException(0, e.getMessage(), HttpMethod.POST, e, null, template.request()); + private boolean requestContainsNonOauthAuthorization(RequestTemplate template) { + Collection authorizations = template.headers().get("Authorization"); + if (authorizations == null) { + return false; } + return !authorizations.stream() + .anyMatch(authHeader -> !authHeader.equalsIgnoreCase("Bearer")); + } + + private synchronized void updateAccessToken() { + OAuth2AccessToken accessTokenResponse; + accessTokenResponse = getOAuth2AccessToken(); if (accessTokenResponse != null && accessTokenResponse.getAccessToken() != null) { setAccessToken(accessTokenResponse.getAccessToken(), accessTokenResponse.getExpiresIn()); if (accessTokenListener != null) { @@ -70,9 +72,18 @@ public abstract class OAuth implements RequestInterceptor { } public synchronized String getAccessToken() { + // If first time, get the token + if (expirationTimeMillis == null || System.currentTimeMillis() >= expirationTimeMillis) { + updateAccessToken(); + } return accessToken; } + /** + * Manually sets the access token + * @param accessToken The access token + * @param expiresIn Seconds until the token expires + */ public synchronized void setAccessToken(String accessToken, Integer expiresIn) { this.accessToken = accessToken; this.expirationTimeMillis = expiresIn == null ? null : System.currentTimeMillis() + expiresIn * MILLIS_PER_SECOND; diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/ApiKeyAuthTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/ApiKeyAuthTest.java new file mode 100644 index 000000000000..46094f103233 --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/ApiKeyAuthTest.java @@ -0,0 +1,110 @@ +package org.openapitools.client.api; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.auth.ApiKeyAuth; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ApiKeyAuthTest { + + private static StoreApi api; + + private static WireMockServer wm = new WireMockServer(options().dynamicPort()); + + @BeforeAll + static void setup() { + wm.start(); + } + + @AfterAll + static void shutdown() { + wm.shutdown(); + } + + @Test + void keyInQueryParameter() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + ApiKeyAuth apiKeyAuth = new ApiKeyAuth("query", "api_key"); + apiClient.addAuthorization("api_key", apiKeyAuth); + apiClient.setApiKey("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"); + + api = apiClient.buildClient(StoreApi.class); + + wm.stubFor(get(urlPathEqualTo("/store/inventory")) + .withQueryParam("api_key", equalTo("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + } + + @Test + void keyInHeader() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + ApiKeyAuth apiKeyAuth = new ApiKeyAuth("header", "api_key"); + apiClient.addAuthorization("api_key", apiKeyAuth); + apiClient.setApiKey("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"); + + api = apiClient.buildClient(StoreApi.class); + + wm.stubFor(get(urlEqualTo("/store/inventory")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("api_key", equalTo("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + } + + @Test + void keyInCookie() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + ApiKeyAuth apiKeyAuth = new ApiKeyAuth("cookie", "api_key"); + apiClient.addAuthorization("api_key", apiKeyAuth); + apiClient.setApiKey("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"); + + api = apiClient.buildClient(StoreApi.class); + + wm.stubFor(get(urlEqualTo("/store/inventory")) + .withHeader("Accept", equalTo("application/json")) + .withCookie("api_key", equalTo("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + } +} diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/HttpBasicAuthTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/HttpBasicAuthTest.java new file mode 100644 index 000000000000..f0b1dad51a5d --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/HttpBasicAuthTest.java @@ -0,0 +1,58 @@ +package org.openapitools.client.api; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.auth.HttpBasicAuth; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class HttpBasicAuthTest { + + private static StoreApi api; + + private static WireMockServer wm = new WireMockServer(options().dynamicPort()); + + @BeforeAll + static void setup() { + wm.start(); + } + + @AfterAll + static void shutdown() { + wm.shutdown(); + } + + @Test + void httpBasicAuth() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + HttpBasicAuth httpBasicAuth = new HttpBasicAuth(); + apiClient.addAuthorization("basic", httpBasicAuth); + apiClient.setCredentials("username", "password"); + + api = apiClient.buildClient(StoreApi.class); + + wm.stubFor(get(urlPathEqualTo("/store/inventory")) + .withHeader("Authorization", equalTo("Basic dXNlcm5hbWU6cGFzc3dvcmQ=")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + } +} diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/HttpBearerAuthTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/HttpBearerAuthTest.java new file mode 100644 index 000000000000..6d927fc305c1 --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/HttpBearerAuthTest.java @@ -0,0 +1,58 @@ +package org.openapitools.client.api; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.auth.HttpBearerAuth; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class HttpBearerAuthTest { + + private static StoreApi api; + + private static WireMockServer wm = new WireMockServer(options().dynamicPort()); + + @BeforeAll + static void setup() { + wm.start(); + } + + @AfterAll + static void shutdown() { + wm.shutdown(); + } + + @Test + void httpBearer() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + HttpBearerAuth httpBearerAuth = new HttpBearerAuth("Bearer"); + apiClient.addAuthorization("bearer", httpBearerAuth); + apiClient.setBearerToken("MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"); + + api = apiClient.buildClient(StoreApi.class); + + wm.stubFor(get(urlPathEqualTo("/store/inventory")) + .withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + } +} diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthClientCredentialsGrantTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthClientCredentialsGrantTest.java new file mode 100644 index 000000000000..df5f7c1f3d50 --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthClientCredentialsGrantTest.java @@ -0,0 +1,72 @@ +package org.openapitools.client.api; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.auth.OauthClientCredentialsGrant; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class OauthClientCredentialsGrantTest { + + private static StoreApi api; + + private static WireMockServer wm = new WireMockServer(options().dynamicPort()); + + @BeforeAll + static void setup() { + wm.start(); + + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + OauthClientCredentialsGrant oauthClientCredentialsGrant = new OauthClientCredentialsGrant(wm.baseUrl() + "/auth", wm.baseUrl() + "/token", "read"); + oauthClientCredentialsGrant.configure("client_id", "client_secret"); + apiClient.addAuthorization("oauth", oauthClientCredentialsGrant); + + api = apiClient.buildClient(StoreApi.class); + } + + @AfterAll + static void shutdown() { + wm.shutdown(); + } + + @Test + void oauthClientCredentialsTest() { + wm.stubFor(post(urlEqualTo("/token")) + .withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials")) + .willReturn(ok("{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" + + " \"scope\":\"read\"\n" + + "}"))); + + wm.stubFor(get(urlEqualTo("/store/inventory")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + + wm.verify(exactly(1), getRequestedFor(urlEqualTo("/store/inventory"))); + wm.verify(exactly(1), postRequestedFor(urlEqualTo("/token"))); + assertThat(wm.getAllServeEvents().size(), is(2)); + } +} diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthPasswordGrantTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthPasswordGrantTest.java new file mode 100644 index 000000000000..32f839c65119 --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthPasswordGrantTest.java @@ -0,0 +1,71 @@ +package org.openapitools.client.api; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.auth.OauthPasswordGrant; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class OauthPasswordGrantTest { + + private static StoreApi api; + + private static WireMockServer wm = new WireMockServer(options().dynamicPort()); + + @BeforeAll + static void setup() { + wm.start(); + + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + OauthPasswordGrant oauthPasswordGrant = new OauthPasswordGrant(wm.baseUrl() + "/token", "read"); + oauthPasswordGrant.configure("username", "password", "client_id", "client_secret"); + apiClient.addAuthorization("oauth", oauthPasswordGrant); + + api = apiClient.buildClient(StoreApi.class); + } + + @AfterAll + static void shutdown() { + wm.shutdown(); + } + + @Test + void oauthPasswordGrant() { + wm.stubFor(post(urlEqualTo("/token")) + .withRequestBody(equalTo("username=username&password=password&scope=read&grant_type=password&client_id=client_id&client_secret=client_secret")) + .willReturn(ok("{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" + + " \"scope\":\"read\"\n" + + "}"))); + + wm.stubFor(get(urlEqualTo("/store/inventory")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + + wm.verify(exactly(1), getRequestedFor(urlEqualTo("/store/inventory"))); + wm.verify(exactly(1), postRequestedFor(urlEqualTo("/token"))); + } +} diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthRetryAuthErrorsTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthRetryAuthErrorsTest.java new file mode 100644 index 000000000000..6ccf6f22f215 --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/api/OauthRetryAuthErrorsTest.java @@ -0,0 +1,160 @@ +package org.openapitools.client.api; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.stubbing.Scenario; +import feign.FeignException; +import feign.RetryableException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.auth.OauthClientCredentialsGrant; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OauthRetryAuthErrorsTest { + + private static StoreApi api; + + private WireMockServer wm = new WireMockServer(options().dynamicPort()); + + @BeforeEach + void setup() { + wm.start(); + + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(wm.baseUrl()); + + OauthClientCredentialsGrant oauthClientCredentialsGrant = new OauthClientCredentialsGrant(null, wm.baseUrl() + "/token", "read"); + oauthClientCredentialsGrant.configure("client_id", "client_secret"); + apiClient.addAuthorization("oauth", oauthClientCredentialsGrant); + + api = apiClient.buildClient(StoreApi.class); + } + + @AfterEach + void shutdown() { + wm.shutdown(); + } + + @Test + void retryableAuthenticationException() { + //First request to fetch the token returns an already expired token + //Just to mock the scenario where already have a token in memory but it expires before the request reaches the server + wm.stubFor(post(urlEqualTo("/token")) + .withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials")) + .inScenario("Retry token") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(ok("{\n" + + " \"access_token\":\"EXPIRED_TOKEN\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":0,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" + + " \"scope\":\"read\"\n" + + "}"))); + + //This token request will be triggered by the RetryableException + wm.stubFor(post(urlEqualTo("/token")) + .withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials")) + .inScenario("Retry token") + .whenScenarioStateIs("After fetching new token") + .willReturn(ok("{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" + + " \"scope\":\"read\"\n" + + "}"))); + + //First request will fail with a 401 + //Simulates a token that expired before reaching the server + wm.stubFor(get(urlEqualTo("/store/inventory")) + .inScenario("Retry token") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("After fetching new token") + .withHeader("Accept", equalTo("application/json")) + .withHeader("Authorization", equalTo("Bearer EXPIRED_TOKEN")) + .willReturn(aResponse().withStatus(401))); + + //The second request sends a newly generated token + wm.stubFor(get(urlEqualTo("/store/inventory")) + .inScenario("Retry token") + .whenScenarioStateIs("After fetching new token") + .withHeader("Accept", equalTo("application/json")) + .withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .willReturn(ok("{\n" + + " \"prop1\": 1,\n" + + " \"prop2\": 2\n" + + "}"))); + + Map inventory = api.getInventory(); + + assertThat(inventory.keySet().size(), is(2)); + assertThat(inventory.get("prop1"), is(1)); + assertThat(inventory.get("prop2"), is(2)); + + wm.verify(exactly(2), getRequestedFor(urlEqualTo("/store/inventory"))); + wm.verify(exactly(2), postRequestedFor(urlEqualTo("/token"))); + } + + @Test + void retryableAuthenticationExhaustedRetries() { + //First request to fetch the token returns an already expired token + //Just to mock the scenario where already have a token in memory but it expires before the request reaches the server + wm.stubFor(post(urlEqualTo("/token")) + .withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials")) + .inScenario("Retry token") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(ok("{\n" + + " \"access_token\":\"EXPIRED_TOKEN\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":0,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" + + " \"scope\":\"read\"\n" + + "}"))); + + //This token request will be triggered by the RetryableException + wm.stubFor(post(urlEqualTo("/token")) + .withRequestBody(equalTo("client_id=client_id&client_secret=client_secret&scope=read&grant_type=client_credentials")) + .inScenario("Retry token") + .whenScenarioStateIs("After fetching new token") + .willReturn(ok("{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n" + + " \"scope\":\"read\"\n" + + "}"))); + + //First request will fail with a 401 + //Simulates a token that expired before reaching the server + wm.stubFor(get(urlEqualTo("/store/inventory")) + .inScenario("Retry token") + .whenScenarioStateIs(Scenario.STARTED) + .willSetStateTo("After fetching new token") + .withHeader("Accept", equalTo("application/json")) + .withHeader("Authorization", equalTo("Bearer EXPIRED_TOKEN")) + .willReturn(aResponse().withStatus(401))); + + //Second request also fails with a 401, in this case the 401 is not related with an expired token + wm.stubFor(get(urlEqualTo("/store/inventory")) + .inScenario("Retry token") + .whenScenarioStateIs("After fetching new token") + .withHeader("Accept", equalTo("application/json")) + .withHeader("Authorization", equalTo("Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3")) + .willReturn(aResponse().withStatus(401))); + + RetryableException retryableException = assertThrows(RetryableException.class, () -> api.getInventory()); + assertThat(retryableException.getCause(), is(instanceOf(FeignException.Unauthorized.class))); + + wm.verify(exactly(2), getRequestedFor(urlEqualTo("/store/inventory"))); + wm.verify(exactly(2), postRequestedFor(urlEqualTo("/token"))); + } +} diff --git a/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/auth/ApiErrorDecoderTest.java b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/auth/ApiErrorDecoderTest.java new file mode 100644 index 000000000000..203b67d64a82 --- /dev/null +++ b/samples/client/petstore/java/feign/src/test/java/org/openapitools/client/auth/ApiErrorDecoderTest.java @@ -0,0 +1,46 @@ +package org.openapitools.client.auth; + +import feign.FeignException; +import feign.Request; +import feign.Response; +import feign.RetryableException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +class ApiErrorDecoderTest { + + private final ApiErrorDecoder apiErrorDecoder = new ApiErrorDecoder(); + + @Test + void decode400() { + Response response = getDummyResponse(400); + + FeignException.BadRequest badRequest = (FeignException.BadRequest) apiErrorDecoder.decode("GET", response); + assertThat(badRequest.status(), is(400)); + } + + @ParameterizedTest + @ValueSource(ints = {401, 403}) + void decodeAuthorizationErrors(Integer httpStatus) { + Response response = getDummyResponse(httpStatus); + + RetryableException retryableException = (RetryableException) apiErrorDecoder.decode("GET", response); + assertThat(retryableException.status(), is(httpStatus)); + assertThat(retryableException.retryAfter(), is(nullValue())); + } + + private Response getDummyResponse(Integer httpStatus) { + Request request = Request.create(Request.HttpMethod.GET, "http://localhost", Collections.emptyMap(), Request.Body.empty(), null); + return Response.builder() + .status(httpStatus) + .request(request) + .build(); + } +} \ No newline at end of file