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;
+ }
+}