changeset 2697:7184fbd4e41f

Add http service with support for keycloak auth Reviewed-by: jerboaa, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-June/023707.html
author Jie Kang <jkang@redhat.com>
date Wed, 21 Jun 2017 14:02:39 -0400
parents ed4beec29d96
children 11a6b082cd1e
files agent/core/pom.xml agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentConfigsUtils.java agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentProperties.java agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentStartupConfiguration.java agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java agent/core/src/main/java/com/redhat/thermostat/agent/http/internal/keycloak/KeycloakAccessToken.java agent/core/src/test/java/com/redhat/thermostat/agent/http/HttpRequestServiceTest.java distribution/config/agent.properties plugins/vm-gc/common/pom.xml plugins/vm-gc/common/src/main/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOImpl.java plugins/vm-gc/common/src/test/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOImplTest.java
diffstat 11 files changed, 658 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/agent/core/pom.xml	Tue Jun 20 14:44:16 2017 -0400
+++ b/agent/core/pom.xml	Wed Jun 21 14:02:39 2017 -0400
@@ -105,6 +105,11 @@
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
     </dependency>
+    <!-- declarative services -->
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.scr.annotations</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
@@ -120,6 +125,8 @@
             <Bundle-Activator>com.redhat.thermostat.agent.internal.Activator</Bundle-Activator>
             <Export-Package>
                 com.redhat.thermostat.agent,
+                com.redhat.thermostat.agent.http,
+                com.redhat.thermostat.agent.http.internal.keycloak,
                 com.redhat.thermostat.agent.config,
                 com.redhat.thermostat.agent.dao,
                 com.redhat.thermostat.agent.utils,
@@ -138,6 +145,19 @@
       </plugin>
 
       <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-scr-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>generate-scr-scrdescriptor</id>
+            <goals>
+              <goal>scr</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-resources-plugin</artifactId>
         <executions>
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentConfigsUtils.java	Tue Jun 20 14:44:16 2017 -0400
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentConfigsUtils.java	Wed Jun 21 14:02:39 2017 -0400
@@ -92,6 +92,18 @@
             configuration.setPurge(true);
         }
 
+        Boolean keycloakEnabled = Boolean.valueOf(properties.getProperty(AgentProperties.KEYCLOAK_ENABLED.name()));
+        if (keycloakEnabled != null) {
+            configuration.setKeycloakEnabled(keycloakEnabled);
+
+            configuration.setKeycloakRealm(properties.getProperty(AgentProperties.KEYCLOAK_REALM.name()));
+            configuration.setKeycloakUrl(properties.getProperty(AgentProperties.KEYCLOAK_URL.name()));
+            configuration.setKeycloakClient(properties.getProperty(AgentProperties.KEYCLOAK_CLIENT.name()));
+            configuration.setKeycloakUsername(properties.getProperty(AgentProperties.KEYCLOAK_USERNAME.name()));
+            configuration.setKeycloakPassword(properties.getProperty(AgentProperties.KEYCLOAK_PASSWORD.name()));
+        } else {
+            configuration.setKeycloakEnabled(false);
+        }
     }
 }
 
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentProperties.java	Tue Jun 20 14:44:16 2017 -0400
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentProperties.java	Wed Jun 21 14:02:39 2017 -0400
@@ -40,5 +40,12 @@
 
     DB_URL,
     SAVE_ON_EXIT,
+
+    KEYCLOAK_ENABLED,
+    KEYCLOAK_URL,
+    KEYCLOAK_REALM,
+    KEYCLOAK_CLIENT,
+    KEYCLOAK_USERNAME,
+    KEYCLOAK_PASSWORD
 }
 
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentStartupConfiguration.java	Tue Jun 20 14:44:16 2017 -0400
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentStartupConfiguration.java	Wed Jun 21 14:02:39 2017 -0400
@@ -41,19 +41,26 @@
 public class AgentStartupConfiguration implements StartupConfiguration {
 
     private boolean purge;
-    private String url;
+    private String dbUrl;
     private long startTime;
+
+    private boolean keycloakEnabled;
+    private String keycloakUrl;
+    private String keycloakRealm;
+    private String keycloakClient;
+    private String keycloakUsername;
+    private String keycloakPassword;
     
     AgentStartupConfiguration() {
     }
     
     @Override
     public String getDBConnectionString() {
-        return url;
+        return dbUrl;
     }
 
     public void setDatabaseURL(String url) {
-        this.url = url;
+        this.dbUrl = url;
     }
     
     // TODO: that should be a friend, we only want the Service to set this value
@@ -72,5 +79,53 @@
     public boolean purge() {
         return purge;
     }
+
+    public String getKeycloakUrl() {
+        return keycloakUrl;
+    }
+
+    public void setKeycloakUrl(String keycloakUrl) {
+        this.keycloakUrl = keycloakUrl;
+    }
+
+    public String getKeycloakClient() {
+        return keycloakClient;
+    }
+
+    public void setKeycloakClient(String keycloakClient) {
+        this.keycloakClient = keycloakClient;
+    }
+
+    public String getKeycloakUsername() {
+        return keycloakUsername;
+    }
+
+    public void setKeycloakUsername(String keycloakUsername) {
+        this.keycloakUsername = keycloakUsername;
+    }
+
+    public String getKeycloakPassword() {
+        return keycloakPassword;
+    }
+
+    public void setKeycloakPassword(String keycloakPassword) {
+        this.keycloakPassword = keycloakPassword;
+    }
+
+    public boolean isKeycloakEnabled() {
+        return keycloakEnabled;
+    }
+
+    public void setKeycloakEnabled(boolean keycloakEnabled) {
+        this.keycloakEnabled = keycloakEnabled;
+    }
+
+    public String getKeycloakRealm() {
+        return keycloakRealm;
+    }
+
+    public void setKeycloakRealm(String keycloakRealm) {
+        this.keycloakRealm = keycloakRealm;
+    }
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java	Wed Jun 21 14:02:39 2017 -0400
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.agent.http;
+
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.agent.config.AgentConfigsUtils;
+import com.redhat.thermostat.agent.config.AgentStartupConfiguration;
+import com.redhat.thermostat.agent.http.internal.keycloak.KeycloakAccessToken;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+
+@Component
+@Service(value = HttpRequestService.class)
+public class HttpRequestService {
+    private static final Logger logger = LoggingUtils.getLogger(HttpRequestService.class);
+
+    private static final String KEYCLOAK_TOKEN_SERVICE = "/auth/realms/__REALM__/protocol/openid-connect/token";
+    private static final String KEYCLOAK_CONTENT_TYPE = "application/x-www-form-urlencoded";
+
+    private Gson gson = new GsonBuilder().create();
+    private HttpClient client;
+    private AgentStartupConfiguration agentStartupConfiguration;
+
+    private KeycloakAccessToken keycloakAccessToken;
+
+    public HttpRequestService() {
+        this(new HttpClient(), AgentConfigsUtils.createAgentConfigs());
+    }
+
+    HttpRequestService(HttpClient client, AgentStartupConfiguration agentStartupConfiguration) {
+        this.client = client;
+        this.agentStartupConfiguration = agentStartupConfiguration;
+    }
+
+    @Activate
+    public void activate() {
+        try {
+            client.start();
+
+            logger.log(Level.FINE, "HttpRequestService activated");
+        } catch (Exception e) {
+            logger.log(Level.SEVERE, "HttpRequestService failed to start correctly. Behaviour undefined.", e);
+        }
+    }
+
+    /**
+     * Send a HTTP request
+     * @param jsonPayload The payload to send, or null if no payload
+     * @param url The complete url to send to
+     * @param requestType The HTTP request type: GET, PUT, POST or DELETE
+     * @return
+     */
+    public ContentResponse sendHttpRequest(String jsonPayload, String url, HttpMethod requestType) throws IOException {
+        Request request = client.newRequest(url);
+        if (jsonPayload != null) {
+            request.content(new StringContentProvider(jsonPayload), "application/json");
+        }
+
+        request.method(requestType);
+
+        if (agentStartupConfiguration.isKeycloakEnabled()) {
+            request.header("Authorization", "Bearer " + getAccessToken());
+        }
+
+        try {
+            return request.send();
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            return null;
+        }
+    }
+
+    private String getAccessToken() throws IOException {
+        if (keycloakAccessToken == null) {
+            keycloakAccessToken = acquireKeycloakToken();
+        } else if (isKeycloakTokenExpired()) {
+            logger.log(Level.FINE, "Keycloak Token expired attempting to reacquire via refresh_token");
+            keycloakAccessToken = refreshKeycloakToken();
+
+            if (keycloakAccessToken == null) {
+                logger.log(Level.WARNING, "Unable to refresh Keycloak token, attempting to acquire new token");
+                keycloakAccessToken = acquireKeycloakToken();
+            }
+
+            if (keycloakAccessToken == null) {
+                logger.log(Level.SEVERE, "Unable to reacquire KeycloakToken.");
+                throw new IOException("Keycloak token expired and attempt to refresh and reacquire Keycloak token failed.");
+            }
+        }
+
+        return keycloakAccessToken.getAccessToken();
+    }
+
+    private boolean isKeycloakTokenExpired() {
+        return System.nanoTime() > TimeUnit.NANOSECONDS.convert(keycloakAccessToken.getExpiresIn(), TimeUnit.SECONDS) + keycloakAccessToken.getAcquireTime();
+    }
+
+    private KeycloakAccessToken acquireKeycloakToken() {
+        return requestKeycloakToken(getKeycloakAccessPayload());
+    }
+
+    private KeycloakAccessToken refreshKeycloakToken() {
+        return requestKeycloakToken(getKeycloakRefreshPayload());
+    }
+
+    private KeycloakAccessToken requestKeycloakToken(String payload) {
+        String url = agentStartupConfiguration.getKeycloakUrl() + KEYCLOAK_TOKEN_SERVICE.replace("__REALM__", agentStartupConfiguration.getKeycloakRealm());
+        Request request = client.newRequest(url);
+        request.content(new StringContentProvider(payload), KEYCLOAK_CONTENT_TYPE);
+        request.method(HttpMethod.POST);
+
+        try {
+            ContentResponse response = request.send();
+            if (response.getStatus() == HttpStatus.OK_200) {
+
+                String content = response.getContentAsString();
+
+                keycloakAccessToken = gson.fromJson(content, KeycloakAccessToken.class);
+                keycloakAccessToken.setAcquireTime(System.nanoTime());
+
+                logger.log(Level.FINE, "Keycloak Token acquired");
+                return keycloakAccessToken;
+            } else {
+                logger.log(Level.WARNING, "Failed to acquire Keycloak token: " + response.getStatus() + " " + response.getReason());
+            }
+        } catch (Exception e) {
+            logger.log(Level.WARNING, "Failed to acquire Keycloak token", e);
+        }
+        return null;
+    }
+
+    private String getKeycloakAccessPayload() {
+        return "grant_type=password&client_id=" + agentStartupConfiguration.getKeycloakClient() +
+                "&username=" + agentStartupConfiguration.getKeycloakUsername() +
+                "&password=" + agentStartupConfiguration.getKeycloakPassword();
+    }
+
+    private String getKeycloakRefreshPayload() {
+        return "grant_type=refresh_token&client_id=" + agentStartupConfiguration.getKeycloakClient() +
+                "&refresh_token=" + keycloakAccessToken.getRefreshToken();
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/http/internal/keycloak/KeycloakAccessToken.java	Wed Jun 21 14:02:39 2017 -0400
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.agent.http.internal.keycloak;
+
+import com.google.gson.annotations.SerializedName;
+
+public class KeycloakAccessToken {
+    @SerializedName("access_token")
+    private String accessToken;
+
+    // In seconds
+    @SerializedName("expires_in")
+    private long expiresIn;
+
+    // In seconds
+    @SerializedName("refresh_expires_in")
+    private long refreshExpiresIn;
+
+    @SerializedName("refresh_token")
+    private String refreshToken;
+
+    // In nanoseconds
+    private transient long acquireTime;
+
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public Long getExpiresIn() {
+        return expiresIn;
+    }
+
+    public Long getRefreshExpiresIn() {
+        return refreshExpiresIn;
+    }
+
+    public String getRefreshToken() {
+        return refreshToken;
+    }
+
+    public long getAcquireTime() {
+        return acquireTime;
+    }
+
+    public void setAcquireTime(long acquireTime) {
+        this.acquireTime = acquireTime;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/test/java/com/redhat/thermostat/agent/http/HttpRequestServiceTest.java	Wed Jun 21 14:02:39 2017 -0400
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.agent.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.redhat.thermostat.agent.config.AgentStartupConfiguration;
+
+public class HttpRequestServiceTest {
+    private String payload = "{}";
+    private String url = "http://127.0.0.1:30000/test";
+    private HttpMethod method = HttpMethod.POST;
+    private String keycloakUrl = "http://127.0.0.1:31000/keycloak";
+
+    private HttpClient client;
+    private Request httpRequest;
+
+    @Before
+    public void setup() throws InterruptedException, ExecutionException, TimeoutException {
+        client = mock(HttpClient.class);
+        httpRequest = mock(Request.class);
+        when(client.newRequest(eq(url))).thenReturn(httpRequest);
+        when(httpRequest.send()).thenReturn(mock(ContentResponse.class));
+    }
+
+    @Test
+    public void testRequestWithoutKeycloak() throws InterruptedException, ExecutionException, TimeoutException, IOException {
+        AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class);
+        when(configuration.isKeycloakEnabled()).thenReturn(false);
+
+        HttpRequestService service = new HttpRequestService(client, configuration);
+
+        service.sendHttpRequest(payload, url, method);
+
+        verify(configuration).isKeycloakEnabled();
+        verifyHttpRequest(httpRequest);
+    }
+
+    @Test
+    public void testRequestWithKeycloak() throws InterruptedException, ExecutionException, TimeoutException, IOException {
+        AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class);
+        setupKeycloakConfig(configuration);
+
+        HttpRequestService service = new HttpRequestService(client, configuration);
+
+        Request keycloakRequest = mock(Request.class);
+        setupKeycloakRequest(keycloakRequest);
+
+        service.sendHttpRequest(payload, url, method);
+
+        verify(configuration).isKeycloakEnabled();
+        verifyHttpRequest(httpRequest);
+
+        verify(httpRequest).header(eq("Authorization"), eq("Bearer access"));
+        verifyKeycloakAcquire(keycloakRequest);
+    }
+
+    @Test
+    public void testRequestWithKeycloakRefresh() throws InterruptedException, ExecutionException, TimeoutException, IOException {
+        AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class);
+        setupKeycloakConfig(configuration);
+
+        HttpRequestService service = new HttpRequestService(client, configuration);
+
+        Request keycloakRequest = mock(Request.class);
+        setupKeycloakRequest(keycloakRequest);
+
+        service.sendHttpRequest(payload, url, method);
+
+        verify(configuration).isKeycloakEnabled();
+        verifyHttpRequest(httpRequest);
+
+        verify(httpRequest).header(eq("Authorization"), eq("Bearer access"));
+
+        verifyKeycloakAcquire(keycloakRequest);
+
+        service.sendHttpRequest(payload, url, method);
+
+
+        ArgumentCaptor<StringContentProvider> payloadCaptor = ArgumentCaptor.forClass(StringContentProvider.class);
+        verify(keycloakRequest, times(2)).content(payloadCaptor.capture(), eq("application/x-www-form-urlencoded"));
+        verify(keycloakRequest, times(2)).method(eq(method));
+        verify(keycloakRequest, times(2)).send();
+
+        String expected = "grant_type=refresh_token&client_id=client&refresh_token=refresh";
+
+        StringContentProvider provider = payloadCaptor.getValue();
+        for (ByteBuffer buffer : provider) {
+            byte[] bytes = buffer.array();
+            String content = new String(bytes, StandardCharsets.UTF_8);
+            assertEquals(expected, content);
+        }
+    }
+
+    @Test
+    public void testRequestWithNullPayload() throws InterruptedException, ExecutionException, TimeoutException, IOException {
+        AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class);
+        when(configuration.isKeycloakEnabled()).thenReturn(false);
+
+        HttpRequestService service = new HttpRequestService(client, configuration);
+
+        service.sendHttpRequest(null, url, method);
+
+        verify(client).newRequest(url);
+        verify(configuration).isKeycloakEnabled();
+
+        verify(httpRequest, times(0)).content(any(StringContentProvider.class), anyString());
+        verify(httpRequest).method(eq(method));
+        verify(httpRequest).send();
+    }
+
+    private void verifyHttpRequest(Request httpRequest) throws InterruptedException, ExecutionException, TimeoutException {
+        verify(client).newRequest(url);
+        verify(httpRequest).content(any(StringContentProvider.class), eq("application/json"));
+        verify(httpRequest).method(eq(method));
+        verify(httpRequest).send();
+    }
+
+    private void setupKeycloakConfig(AgentStartupConfiguration configuration) {
+        when(configuration.isKeycloakEnabled()).thenReturn(true);
+        when(configuration.getKeycloakUrl()).thenReturn(keycloakUrl);
+        when(configuration.getKeycloakClient()).thenReturn("client");
+        when(configuration.getKeycloakRealm()).thenReturn("realm");
+        when(configuration.getKeycloakUsername()).thenReturn("username");
+        when(configuration.getKeycloakPassword()).thenReturn("password");
+    }
+
+    private void setupKeycloakRequest(Request keycloakRequest) throws InterruptedException, ExecutionException, TimeoutException {
+        when(client.newRequest(keycloakUrl + "/auth/realms/realm/protocol/openid-connect/token")).thenReturn(keycloakRequest);
+
+        ContentResponse response = mock(ContentResponse.class);
+        when(response.getStatus()).thenReturn(HttpStatus.OK_200);
+
+        String keycloakToken = "{" +
+                "\"access_token\": \"access\"," +
+                "\"expires_in\": 0," +
+                "\"refresh_expires_in\": 2," +
+                "\"refresh_token\": \"refresh\"," +
+                "\"token_type\": \"bearer\"," +
+                "\"id_token\": \"id\"," +
+                "\"not-before-policy\": 3," +
+                "\"session_state\": \"state\"" +
+                "}";
+
+        when(response.getContentAsString()).thenReturn(keycloakToken);
+        when(keycloakRequest.send()).thenReturn(response);
+    }
+
+    private void verifyKeycloakAcquire(Request keycloakRequest) throws InterruptedException, ExecutionException, TimeoutException {
+        ArgumentCaptor<StringContentProvider> payloadCaptor = ArgumentCaptor.forClass(StringContentProvider.class);
+        verify(keycloakRequest).content(payloadCaptor.capture(), eq("application/x-www-form-urlencoded"));
+        verify(keycloakRequest).method(eq(method));
+        verify(keycloakRequest).send();
+
+        String expected = "grant_type=password&client_id=client&username=username&password=password";
+
+        StringContentProvider provider = payloadCaptor.getValue();
+        for (ByteBuffer buffer : provider) {
+            byte[] bytes = buffer.array();
+            String content = new String(bytes, StandardCharsets.UTF_8);
+            assertEquals(expected, content);
+        }
+
+    }
+}
--- a/distribution/config/agent.properties	Tue Jun 20 14:44:16 2017 -0400
+++ b/distribution/config/agent.properties	Wed Jun 21 14:02:39 2017 -0400
@@ -1,3 +1,11 @@
 # Indicates if this agent will save its data to the database on exit
 # or rather will purge the db
 SAVE_ON_EXIT=true
+
+# Keycloak server URL used for authentication of http requests
+KEYCLOAK_ENABLED=false
+KEYCLOAK_URL=http://127.0.0.1:31000
+KEYCLOAK_REALM=thermostat
+KEYCLOAK_CLIENT=thermostat-web-client
+KEYCLOAK_USERNAME=tms-write
+KEYCLOAK_PASSWORD=tms-pass
\ No newline at end of file
--- a/plugins/vm-gc/common/pom.xml	Tue Jun 20 14:44:16 2017 -0400
+++ b/plugins/vm-gc/common/pom.xml	Wed Jun 21 14:02:39 2017 -0400
@@ -131,6 +131,11 @@
     </dependency>
     <dependency>
       <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-agent-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-storage-core</artifactId>
       <version>${project.version}</version>
     </dependency>
--- a/plugins/vm-gc/common/src/main/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOImpl.java	Tue Jun 20 14:44:16 2017 -0400
+++ b/plugins/vm-gc/common/src/main/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOImpl.java	Wed Jun 21 14:02:39 2017 -0400
@@ -39,28 +39,24 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+
+import com.redhat.thermostat.agent.http.HttpRequestService;
 import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource;
 import com.redhat.thermostat.common.plugins.PluginConfiguration;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.vm.gc.common.VmGcStatDAO;
 import com.redhat.thermostat.vm.gc.common.model.VmGcStat;
 
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Reference;
-import org.apache.felix.scr.annotations.Service;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.util.StringContentProvider;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.HttpStatus;
-
 @Component
 @Service(value = VmGcStatDAO.class)
 public class VmGcStatDAOImpl implements VmGcStatDAO {
@@ -70,24 +66,21 @@
     static final String CONTENT_TYPE = "application/json";
     
     private final JsonHelper jsonHelper;
-    private final HttpHelper httpHelper;
-    private final HttpClient httpClient;
     private final ConfigurationCreator configCreator;
     
     @Reference
     private ConfigurationInfoSource configInfoSource;
     private String gatewayURL;
 
+    @Reference
+    private HttpRequestService httpRequestService;
+
     public VmGcStatDAOImpl() {
-        this(new HttpClient(), new JsonHelper(new VmGcStatTypeAdapter()), new HttpHelper(), 
-                new ConfigurationCreator(), null);
+        this(new JsonHelper(new VmGcStatTypeAdapter()), new ConfigurationCreator(), null);
     }
 
-    VmGcStatDAOImpl(HttpClient client, JsonHelper jh, HttpHelper hh, ConfigurationCreator creator, 
-            ConfigurationInfoSource source) {
-        this.httpClient = client;
+    VmGcStatDAOImpl(JsonHelper jh, ConfigurationCreator creator, ConfigurationInfoSource source) {
         this.jsonHelper = jh;
-        this.httpHelper = hh;
         this.configCreator = creator;
         this.configInfoSource = source;
     }
@@ -96,34 +89,37 @@
     void activate() throws Exception {
         PluginConfiguration config = configCreator.create(configInfoSource);
         this.gatewayURL = config.getGatewayURL();
-        
-        httpHelper.startClient(httpClient);
+    }
+
+    protected void bindHttpRequestService(HttpRequestService httpRequestService) {
+        this.httpRequestService = httpRequestService;
+    }
+
+    protected void unbindHttpRequestService(HttpRequestService httpRequestService) {
+        this.httpRequestService = null;
     }
 
     @Override
     public void putVmGcStat(final VmGcStat stat) {
         try {
             String json = jsonHelper.toJson(Arrays.asList(stat));
-            StringContentProvider provider = httpHelper.createContentProvider(json);
+
+            if (null != httpRequestService) {
+                ContentResponse response = httpRequestService.sendHttpRequest(json, gatewayURL, HttpMethod.POST);
 
-            Request httpRequest = httpClient.newRequest(gatewayURL);
-            httpRequest.method(HttpMethod.POST);
-            httpRequest.content(provider, CONTENT_TYPE);
-            sendRequest(httpRequest);
+                int status = response.getStatus();
+                if (status != HttpStatus.OK_200) {
+                    throw new IOException("Gateway returned HTTP status " + String.valueOf(status) + " - " + response.getReason());
+                }
+
+            } else {
+                logger.log(Level.WARNING, "Failed to send VmGcStat information to web gateway. Http service unavailable.");
+            }
         } catch (Exception e) {
             logger.log(Level.WARNING, "Failed to send VmGcStat information to web gateway", e);
         }
     }
 
-    private void sendRequest(Request httpRequest)
-            throws InterruptedException, TimeoutException, ExecutionException, IOException {
-        ContentResponse resp = httpRequest.send();
-        int status = resp.getStatus();
-        if (status != HttpStatus.OK_200) {
-            throw new IOException("Gateway returned HTTP status " + String.valueOf(status) + " - " + resp.getReason());
-        }
-    }
-    
     // For Testing purposes
     static class JsonHelper {
 
@@ -140,18 +136,6 @@
     }
 
     // For Testing purposes
-    static class HttpHelper {
-
-        void startClient(HttpClient httpClient) throws Exception {
-            httpClient.start();
-        }
-
-        StringContentProvider createContentProvider(String content) {
-            return new StringContentProvider(content);
-        }
-    }
-    
-    // For Testing purposes
     static class ConfigurationCreator {
         
         PluginConfiguration create(ConfigurationInfoSource source) {
--- a/plugins/vm-gc/common/src/test/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOImplTest.java	Tue Jun 20 14:44:16 2017 -0400
+++ b/plugins/vm-gc/common/src/test/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOImplTest.java	Wed Jun 21 14:02:39 2017 -0400
@@ -36,28 +36,27 @@
 
 package com.redhat.thermostat.vm.gc.common.internal;
 
+import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyListOf;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
 
-import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.util.StringContentProvider;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.Before;
 import org.junit.Test;
 
+import com.redhat.thermostat.agent.http.HttpRequestService;
 import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource;
 import com.redhat.thermostat.common.plugins.PluginConfiguration;
 import com.redhat.thermostat.vm.gc.common.internal.VmGcStatDAOImpl.ConfigurationCreator;
-import com.redhat.thermostat.vm.gc.common.internal.VmGcStatDAOImpl.HttpHelper;
 import com.redhat.thermostat.vm.gc.common.internal.VmGcStatDAOImpl.JsonHelper;
 import com.redhat.thermostat.vm.gc.common.model.VmGcStat;
 
@@ -68,14 +67,11 @@
     private static final String GATEWAY_URL = "http://example.com/jvm-gc";
     
     private VmGcStat stat;
-    private HttpClient httpClient;
-    private HttpHelper httpHelper;
     private JsonHelper jsonHelper;
-    private StringContentProvider contentProvider;
-    private Request request;
-    private ContentResponse response;
     private VmGcStatDAOImpl dao;
 
+    private HttpRequestService httpRequestService;
+
     @Before
     public void setup() throws Exception {
         stat = new VmGcStat();
@@ -86,25 +82,22 @@
         stat.setVmId("Vm-1");
         stat.setCollectorName("Collector");
 
-        httpClient = mock(HttpClient.class);
-        request = mock(Request.class);
-        when(httpClient.newRequest(anyString())).thenReturn(request);
-        response = mock(ContentResponse.class);
-        when(response.getStatus()).thenReturn(HttpStatus.OK_200);
-        when(request.send()).thenReturn(response);
-
         jsonHelper = mock(JsonHelper.class);
         when(jsonHelper.toJson(anyListOf(VmGcStat.class))).thenReturn(JSON);
-        httpHelper = mock(HttpHelper.class);
-        contentProvider = mock(StringContentProvider.class);
-        when(httpHelper.createContentProvider(anyString())).thenReturn(contentProvider);
-        
+
         ConfigurationInfoSource source = mock(ConfigurationInfoSource.class);
         PluginConfiguration config = mock(PluginConfiguration.class);
         when(config.getGatewayURL()).thenReturn(GATEWAY_URL);
         ConfigurationCreator creator = mock(ConfigurationCreator.class);
         when(creator.create(source)).thenReturn(config);
-        dao = new VmGcStatDAOImpl(httpClient, jsonHelper, httpHelper, creator, source);
+
+        httpRequestService = mock(HttpRequestService.class);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(httpRequestService.sendHttpRequest(anyString(), anyString(), any(HttpMethod.class))).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
+
+        dao = new VmGcStatDAOImpl(jsonHelper, creator, source);
+        dao.bindHttpRequestService(httpRequestService);
     }
 
     @Test
@@ -112,13 +105,9 @@
         dao.activate();
         dao.putVmGcStat(stat);
 
-        verify(httpClient).newRequest(GATEWAY_URL);
-        verify(request).method(HttpMethod.POST);
         verify(jsonHelper).toJson(eq(Arrays.asList(stat)));
-        verify(httpHelper).createContentProvider(JSON);
-        verify(request).content(contentProvider, VmGcStatDAOImpl.CONTENT_TYPE);
-        verify(request).send();
-        verify(response).getStatus();
+
+        verify(httpRequestService.sendHttpRequest(JSON, GATEWAY_URL, HttpMethod.POST), times(1));
     }
 
 }