diff --git a/api-client-spring-boot-autoconfigure/pom.xml b/api-client-spring-boot-autoconfigure/pom.xml index 739f46c..2a71714 100644 --- a/api-client-spring-boot-autoconfigure/pom.xml +++ b/api-client-spring-boot-autoconfigure/pom.xml @@ -19,7 +19,7 @@ 2.8.6 11.1 4.9.1 - 0.0.7 + 0.1.0 false diff --git a/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientAutoConfiguration.java b/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientAutoConfiguration.java index 0713164..5c72488 100644 --- a/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientAutoConfiguration.java +++ b/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientAutoConfiguration.java @@ -1,15 +1,16 @@ package org.azbuilder.api.spring.autoconfigure; import feign.Feign; +import feign.Logger; import feign.okhttp.OkHttpClient; +import feign.slf4j.Slf4jLogger; import org.azbuilder.api.client.RestClient; +import org.azbuilder.api.client.security.azure.ClientCredentialAuthentication; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import feign.slf4j.Slf4jLogger; -import feign.Logger; import feign.gson.GsonDecoder; import feign.gson.GsonEncoder; @@ -23,13 +24,25 @@ public class RestClientAutoConfiguration { @Bean public RestClient restClient(RestClientProperties restClientProperties) { + + ClientCredentialAuthentication clientCredentialAuthentication = new ClientCredentialAuthentication( + restClientProperties.getTenantId(), + restClientProperties.getClientId(), + restClientProperties.getClientSecret(), + restClientProperties.getScope() + ); + + okhttp3.OkHttpClient customHttpClient = new okhttp3.OkHttpClient.Builder() + .authenticator(clientCredentialAuthentication) + .addInterceptor(clientCredentialAuthentication) + .build(); + RestClient restClient = Feign.builder() .encoder(new GsonEncoder()) .decoder(new GsonDecoder()) - .client( new OkHttpClient()) + .client(new OkHttpClient(customHttpClient)) //.logger(new Slf4jLogger()) - //.logLevel(Logger.Level.BASIC) - + //.logLevel(Logger.Level.FULL) .target(RestClient.class, restClientProperties.getUrl()); return restClient; } diff --git a/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientProperties.java b/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientProperties.java index 74d43b4..c9e2ffe 100644 --- a/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientProperties.java +++ b/api-client-spring-boot-autoconfigure/src/main/java/org/azbuilder/api/spring/autoconfigure/RestClientProperties.java @@ -17,4 +17,8 @@ @ConfigurationProperties(prefix = "org.azbuilder.api") public class RestClientProperties { private String url; + private String clientId; + private String clientSecret; + private String tenantId; + private String scope; } diff --git a/api-client-spring-boot-starter-sample/pom.xml b/api-client-spring-boot-starter-sample/pom.xml index 67d469d..e238033 100644 --- a/api-client-spring-boot-starter-sample/pom.xml +++ b/api-client-spring-boot-starter-sample/pom.xml @@ -15,7 +15,7 @@ Demo project for Spring Boot 11 - 0.0.7 + 0.1.0 true diff --git a/api-client-spring-boot-starter-sample/src/test/java/org/azbuilder/api/client/sample/ApiClientStarterSampleApplicationTests.java b/api-client-spring-boot-starter-sample/src/test/java/org/azbuilder/api/client/sample/ApiClientStarterSampleApplicationTests.java index 4a8e6ca..a9a82c0 100644 --- a/api-client-spring-boot-starter-sample/src/test/java/org/azbuilder/api/client/sample/ApiClientStarterSampleApplicationTests.java +++ b/api-client-spring-boot-starter-sample/src/test/java/org/azbuilder/api/client/sample/ApiClientStarterSampleApplicationTests.java @@ -3,9 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; - import static org.junit.jupiter.api.Assertions.assertNotNull; - import org.azbuilder.api.client.RestClient; @SpringBootTest @@ -17,6 +15,7 @@ class ApiClientStarterSampleApplicationTests { @Test void contextLoads() { assertNotNull(restClient); + } } diff --git a/api-client-spring-boot-starter-sample/src/test/resources/application.properties b/api-client-spring-boot-starter-sample/src/test/resources/application.properties index d1110a0..2d5cfed 100644 --- a/api-client-spring-boot-starter-sample/src/test/resources/application.properties +++ b/api-client-spring-boot-starter-sample/src/test/resources/application.properties @@ -1 +1,7 @@ -org.azbuilder.api.url=http://localhost:8080 \ No newline at end of file +org.azbuilder.api.url=http://localhost:8080 +#org.azbuilder.api.clientId=${AzureAdAppClientId} +#org.azbuilder.api.clientSecret=${AzureAdAppClientSecret} +#org.azbuilder.api.tenantId=${AzureAdAppTenantId} +#org.azbuilder.api.scope=${AzureAdAppScope} + +#logging.level.root=INFO \ No newline at end of file diff --git a/api-client-spring-boot-starter/pom.xml b/api-client-spring-boot-starter/pom.xml index 47a9afe..c748268 100644 --- a/api-client-spring-boot-starter/pom.xml +++ b/api-client-spring-boot-starter/pom.xml @@ -13,7 +13,7 @@ UTF-8 - 0.0.7 + 0.1.0 false diff --git a/api-client/pom.xml b/api-client/pom.xml index 0952be2..0490fb4 100644 --- a/api-client/pom.xml +++ b/api-client/pom.xml @@ -15,8 +15,9 @@ Demo project for Spring Boot 11 - 0.0.7 + 0.1.0 11.1 + 3.8.0 false @@ -43,7 +44,21 @@ feign-core ${feign.version} - + + io.github.openfeign.form + feign-form + ${feign-form.version} + + + io.github.openfeign + feign-okhttp + ${feign.version} + + + io.github.openfeign + feign-gson + ${feign.version} + org.springframework.boot spring-boot-starter-test diff --git a/api-client/src/main/java/org/azbuilder/api/client/security/azure/ClientCredentialApi.java b/api-client/src/main/java/org/azbuilder/api/client/security/azure/ClientCredentialApi.java new file mode 100644 index 0000000..1ad219e --- /dev/null +++ b/api-client/src/main/java/org/azbuilder/api/client/security/azure/ClientCredentialApi.java @@ -0,0 +1,14 @@ +package org.azbuilder.api.client.security.azure; + +import feign.Headers; +import feign.Param; +import feign.RequestLine; + +import java.util.Map; + +public interface ClientCredentialApi { + + @RequestLine("GET /{tenantId}/oauth2/v2.0/token") + @Headers("Content-Type: application/x-www-form-urlencoded") + Map getAccessToken(@Param("tenantId") String tenantId, @Param("grant_type") String grantType, @Param("client_id") String clientId, @Param("scope") String scope, @Param("client_secret") String clientSecret); +} diff --git a/api-client/src/main/java/org/azbuilder/api/client/security/azure/ClientCredentialAuthentication.java b/api-client/src/main/java/org/azbuilder/api/client/security/azure/ClientCredentialAuthentication.java new file mode 100644 index 0000000..bcd965f --- /dev/null +++ b/api-client/src/main/java/org/azbuilder/api/client/security/azure/ClientCredentialAuthentication.java @@ -0,0 +1,86 @@ +package org.azbuilder.api.client.security.azure; + +import feign.Feign; +import feign.form.FormEncoder; +import feign.gson.GsonDecoder; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Map; + +@Slf4j +public class ClientCredentialAuthentication implements Authenticator, Interceptor { + + private static final String AZURE_ENDPOINT = "https://login.microsoftonline.com"; + private static final String GRANT_TYPE = "client_credentials"; + + private String tenantId; + private String clientId; + private String clientSecret; + private String scope; + private ClientCredentialApi clientCredentialApi; + private String accessToken = ""; + + public ClientCredentialAuthentication(String tenantId, String clientId, String clientSecret, String scope) { + this.tenantId = tenantId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.scope = scope; + this.clientCredentialApi = Feign.builder() + .encoder(new FormEncoder()) + .decoder(new GsonDecoder()) + .target(ClientCredentialApi.class, AZURE_ENDPOINT); + } + + @Override + public Request authenticate(Route route, Response response) { + log.info("Authentication error {}", response.code()); + synchronized (this) { + this.accessToken = generateAccessToken(); + } + return newRequestWithAccessToken(response.request(), this.accessToken); + } + + @NonNull + private Request newRequestWithAccessToken(@NonNull Request request, String accessToken) { + return request.newBuilder() + .header("Authorization", "Bearer " + accessToken) + .build(); + } + + private String generateAccessToken() { + log.error("Renew Azure Active Directory Token"); + String accessToken = ""; + Map response = clientCredentialApi.getAccessToken( + this.tenantId, + GRANT_TYPE, + this.clientId, + this.scope, + this.clientSecret); + + if (response.containsKey("error")) { + throw new RuntimeException(response.get("error_description")); + } + accessToken = response.get("access_token"); + log.info("Successful acquire new Azure AD access token"); + return accessToken; + } + + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = newRequestWithAccessToken(chain.request(), this.accessToken); + Response response = chain.proceed(request); + + if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + synchronized (this) { + return chain.proceed(newRequestWithAccessToken(request, generateAccessToken())); + } + } + + return response; + } +}