changeset 831:6cd46e3563ce

Implement authentication for cmd channel. Reviewed-by: jerboaa, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-December/004459.html
author Roman Kennke <rkennke@redhat.com>
date Mon, 10 Dec 2012 14:30:36 +0100
parents eaf1fc6abd3d
children 99a6f56a52b1
files agent/command/pom.xml agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ServerHandler.java client/command/pom.xml client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java common/command/src/main/java/com/redhat/thermostat/common/command/Request.java common/command/src/main/java/com/redhat/thermostat/common/command/Response.java distribution/config/commands/agent.properties distribution/config/commands/gui.properties distribution/config/commands/storage.properties distribution/config/commands/webservice.properties distribution/pom.xml pom.xml storage/core/src/main/java/com/redhat/thermostat/storage/core/AuthToken.java storage/core/src/main/java/com/redhat/thermostat/storage/core/SecureStorage.java web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java web/server/pom.xml web/server/src/main/java/com/redhat/thermostat/web/server/TokenManager.java web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java web/server/src/main/webapp/WEB-INF/web.xml web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java web/war/src/main/webapp/WEB-INF/web.xml
diffstat 23 files changed, 654 insertions(+), 25 deletions(-) [+]
line wrap: on
line diff
--- a/agent/command/pom.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/agent/command/pom.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -81,6 +81,12 @@
     </dependency>
 
     <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>${commons-codec.version}</version>
+    </dependency>
+
+    <dependency>
       <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-common-command</artifactId>
       <version>${project.version}</version>
--- a/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ServerHandler.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ServerHandler.java	Mon Dec 10 14:30:36 2012 +0100
@@ -39,6 +39,7 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import org.apache.commons.codec.binary.Base64;
 import org.jboss.netty.channel.Channel;
 import org.jboss.netty.channel.ChannelFuture;
 import org.jboss.netty.channel.ChannelFutureListener;
@@ -46,14 +47,20 @@
 import org.jboss.netty.channel.ExceptionEvent;
 import org.jboss.netty.channel.MessageEvent;
 import org.jboss.netty.channel.SimpleChannelHandler;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
 
 import com.redhat.thermostat.agent.command.ReceiverRegistry;
 import com.redhat.thermostat.agent.command.RequestReceiver;
 import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.Request.RequestType;
 import com.redhat.thermostat.common.command.Response;
-import com.redhat.thermostat.common.command.Request.RequestType;
 import com.redhat.thermostat.common.command.Response.ResponseType;
 import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.storage.core.AuthToken;
+import com.redhat.thermostat.storage.core.SecureStorage;
+import com.redhat.thermostat.storage.core.Storage;
 
 class ServerHandler extends SimpleChannelHandler {
 
@@ -68,13 +75,19 @@
     @Override
     public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
         Request request = (Request) e.getMessage();
-        logger.info("Request received: " + request.getType().toString());
-        RequestReceiver receiver = receivers.getReceiver(request.getReceiver());
+        boolean authSucceeded = authenticateRequestIfNecessary(request);
         Response response = null;
-        if (receiver != null) {
-            response = receiver.receive(request);
+        if (! authSucceeded) {
+            response = new Response(ResponseType.AUTH_FAILED);
         } else {
-            response = new Response(ResponseType.ERROR);
+            logger.info("Request received: " + request.getType().toString());
+            RequestReceiver receiver = receivers.getReceiver(request
+                    .getReceiver());
+            if (receiver != null) {
+                response = receiver.receive(request);
+            } else {
+                response = new Response(ResponseType.ERROR);
+            }
         }
         Channel channel = ctx.getChannel();
         if (channel.isConnected()) {
@@ -86,6 +99,26 @@
         }
     }
 
+    private boolean authenticateRequestIfNecessary(Request request) {
+        BundleContext bCtx = FrameworkUtil.getBundle(getClass()).getBundleContext();
+        ServiceReference storageRef = bCtx.getServiceReference(Storage.class.getName());
+        Storage storage = (Storage) bCtx.getService(storageRef);
+        if (storage instanceof SecureStorage) {
+            return authenticateRequest(request, (SecureStorage) storage);
+        } else {
+            return true;
+        }
+    }
+
+    private boolean authenticateRequest(Request request, SecureStorage storage) {
+        String clientTokenStr = request.getParameter(Request.CLIENT_TOKEN);
+        byte[] clientToken = Base64.decodeBase64(clientTokenStr);
+        String authTokenStr = request.getParameter(Request.AUTH_TOKEN);
+        byte[] authToken = Base64.decodeBase64(authTokenStr);
+        AuthToken token = new AuthToken(authToken, clientToken);
+        return storage.verifyToken(token);
+    }
+
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
         logger.log(Level.WARNING, "Unexpected exception from downstream.", e.getCause());
--- a/client/command/pom.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/client/command/pom.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -72,6 +72,11 @@
     </dependency>
 
     <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>${commons-codec.version}</version>
+    </dependency>
+    <dependency>
       <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-common-core</artifactId>
       <version>${project.version}</version>
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java	Mon Dec 10 14:30:36 2012 +0100
@@ -39,15 +39,23 @@
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 
+import org.apache.commons.codec.binary.Base64;
 import org.jboss.netty.bootstrap.ClientBootstrap;
 import org.jboss.netty.channel.Channel;
 import org.jboss.netty.channel.ChannelFuture;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
 
 import com.redhat.thermostat.client.command.RequestQueue;
 import com.redhat.thermostat.common.command.Request;
 import com.redhat.thermostat.common.command.RequestResponseListener;
 import com.redhat.thermostat.common.command.Response;
 import com.redhat.thermostat.common.command.Response.ResponseType;
+import com.redhat.thermostat.storage.core.AuthToken;
+import com.redhat.thermostat.storage.core.SecureStorage;
+import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.StorageException;
 
 class RequestQueueImpl implements RequestQueue {
 
@@ -64,9 +72,29 @@
 
     @Override
     public void putRequest(Request request) {
+        authenticateRequest(request);
         queue.add(request);
     }
 
+    private void authenticateRequest(Request request) {
+        BundleContext bCtx = FrameworkUtil.getBundle(getClass()).getBundleContext();
+        ServiceReference storageRef = bCtx.getServiceReference(Storage.class.getName());
+        Storage storage = (Storage) bCtx.getService(storageRef);
+        if (storage instanceof SecureStorage) {
+            authenticateRequest(request, (SecureStorage) storage);
+        }
+    }
+
+    private void authenticateRequest(Request request, SecureStorage storage) {
+        try {
+            AuthToken token = storage.generateToken();
+            request.setParameter(Request.CLIENT_TOKEN, Base64.encodeBase64String(token.getClientToken()));
+            request.setParameter(Request.AUTH_TOKEN, Base64.encodeBase64String(token.getToken()));
+        } catch (StorageException ex) {
+            fireComplete(request, new Response(ResponseType.AUTH_FAILED));
+        }
+    }
+
     synchronized void startProcessingRequests() {
         if (!running()) {
             processing = true;
@@ -112,12 +140,17 @@
                 	c.write(request);
                 } else {
                 	Response response  = new Response(ResponseType.ERROR);
-                	// TODO add more information once Response supports parameters.
-                	for (RequestResponseListener listener : request.getListeners()) {
-                		listener.fireComplete(request, response);
-                	}
+                	fireComplete(request, response);
                 }
             }
         }
+
+    }
+
+    private void fireComplete(Request request, Response response) {
+        // TODO add more information once Response supports parameters.
+        for (RequestResponseListener listener : request.getListeners()) {
+            listener.fireComplete(request, response);
+        }
     }
 }
--- a/common/command/src/main/java/com/redhat/thermostat/common/command/Request.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/common/command/src/main/java/com/redhat/thermostat/common/command/Request.java	Mon Dec 10 14:30:36 2012 +0100
@@ -59,6 +59,9 @@
 
     private static final String RECEIVER = "receiver";
 
+    public static final String CLIENT_TOKEN = "client-token";
+    public static final String AUTH_TOKEN = "auth-token";
+
     public Request(RequestType type, SocketAddress target) {
         this.type = type;
         parameters = new TreeMap<>();
--- a/common/command/src/main/java/com/redhat/thermostat/common/command/Response.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/common/command/src/main/java/com/redhat/thermostat/common/command/Response.java	Mon Dec 10 14:30:36 2012 +0100
@@ -48,7 +48,8 @@
         NOK,       // Request has been acknowledged and refused agent-side.
         NOOP,      // Request has been acknowledged, but no action deemed necessary agent-side.
         ERROR,     // An error on the agent side.
-        EXCEPTION; // Exception caught by channel handler.  Agent-side status unknown.
+        EXCEPTION, // Exception caught by channel handler.  Agent-side status unknown.
+        AUTH_FAILED; // When authentication fails in SecureStorage.
     }
 
     ResponseType type;
--- a/distribution/config/commands/agent.properties	Tue Dec 04 20:14:25 2012 -0500
+++ b/distribution/config/commands/agent.properties	Mon Dec 10 14:30:36 2012 +0100
@@ -19,6 +19,7 @@
           thermostat-storage-mongodb-${project.version}.jar, \
           mongo.jar, \
           commons-beanutils.jar, \
+          commons-codec.jar, \
           commons-collections.jar, \
           commons-logging.jar, \
           netty.jar
--- a/distribution/config/commands/gui.properties	Tue Dec 04 20:14:25 2012 -0500
+++ b/distribution/config/commands/gui.properties	Mon Dec 10 14:30:36 2012 +0100
@@ -2,6 +2,7 @@
           mongo.jar, \
           commons-beanutils.jar, \
           commons-collections.jar, \
+          commons-codec.jar, \
           commons-logging.jar, \
           gson.jar, \
           thermostat-web-common-@project.version@.jar, \
--- a/distribution/config/commands/storage.properties	Tue Dec 04 20:14:25 2012 -0500
+++ b/distribution/config/commands/storage.properties	Mon Dec 10 14:30:36 2012 +0100
@@ -3,6 +3,7 @@
           thermostat-agent-cli-@project.version@.jar, \
           thermostat-common-command-@project.version@.jar, \
           thermostat-agent-command-@project.version@.jar, \
+          commons-codec.jar, \
           netty.jar
 
 description = starts and stops the thermostat storage
--- a/distribution/config/commands/webservice.properties	Tue Dec 04 20:14:25 2012 -0500
+++ b/distribution/config/commands/webservice.properties	Mon Dec 10 14:30:36 2012 +0100
@@ -7,6 +7,7 @@
           thermostat-storage-mongodb-${project.version}.jar, \
           mongo.jar, \
           commons-beanutils.jar, \
+          commons-codec.jar, \
           commons-collections.jar, \
           commons-logging.jar, \
           commons-fileupload.jar, \
--- a/distribution/pom.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/distribution/pom.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -205,6 +205,8 @@
             <phase>prepare-package</phase>
             <configuration>
               <target>
+                <symlink link="${project.build.directory}/libs/commons-codec.jar"
+                         resource="${project.build.directory}/libs/commons-codec-1.7.jar" />
                 <symlink link="${project.build.directory}/libs/netty.jar"
                          resource="${project.build.directory}/libs/netty-3.2.4.Final.jar" />
                 <symlink link="${project.build.directory}/libs/jline2.jar"
--- a/pom.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/pom.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -76,6 +76,8 @@
     <commons-io.version>2.4</commons-io.version>
     <commons-collections.version>3.2.1</commons-collections.version>
     <commons-logging.version>1.1.1</commons-logging.version>
+    <commons-codec.version>1.7</commons-codec.version>
+
     <jline.version>2.9</jline.version>
     <lucene.version>3.6.0_1</lucene.version>
     <!--
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/AuthToken.java	Mon Dec 10 14:30:36 2012 +0100
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2012 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.storage.core;
+
+import java.util.Arrays;
+
+
+public class AuthToken {
+
+    private byte[] token;
+    private byte[] clientToken;
+
+    public AuthToken(byte[] token, byte[] clientToken) {
+        this.token = token;
+        this.clientToken = clientToken;
+    }
+
+    public byte[] getToken() {
+        return token;
+    }
+
+    public byte[] getClientToken() {
+        return clientToken;
+    }
+
+    public String toString() {
+        return "AuthToken: client-token: " + Arrays.toString(clientToken) + ", token: " + Arrays.toString(token);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/SecureStorage.java	Mon Dec 10 14:30:36 2012 +0100
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 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.storage.core;
+
+/**
+ * Provides authentication service to the command channel API.
+ *
+ * The protocol works as follows:
+ *
+ * <ol>
+ * <li> The client asks the SecureStorage for an authentication token using {@link #generateToken()}.
+ *      This should happen over an authenticated and secured connection, thus authenticating the client
+ *      in the storage. The returned AuthToken will carry a client-token (that has been generated by the
+ *      client) and an auth-token (generated by the storage). If authentication fails at this stage
+ *      (e.g. because the client does not have the necessary privileges/roles), a StorageException is
+ *      thrown.
+ * </li>
+ * <li> The AuthToken (both parts, client and auth token), are sent together with the command request to
+ *      the command receiver (usually an agent).
+ * </li>
+ * <li> The agent takes the tokens, and calls the SecureStorage's {@link #verifyToken(AuthToken)} to
+ *      verify the validity of the tokens. Again, this needs to happen through an authenticated and
+ *      secured connection, thus authenticating the receiver. The storage verifies that it generated
+ *      the same token for an authenticated client before, and replies with true if it succeeds, and
+ *      false otherwise.
+ */
+public interface SecureStorage {
+
+    /**
+     * Generates a token in the storage that can be used to authenticate cmd channel requests.
+     *
+     * @throws StorageException if authentication fails at this point
+     */
+    AuthToken generateToken() throws StorageException;
+
+    /**
+     * Verifies the specified token in the storage.
+     * 
+     * @return <code>true</code> if authentication succeeded, <code>false</code> otherwise
+     */
+    boolean verifyToken(AuthToken token);
+}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java	Mon Dec 10 14:30:36 2012 +0100
@@ -46,6 +46,7 @@
 import java.lang.reflect.Array;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -53,6 +54,7 @@
 import java.util.Map;
 import java.util.UUID;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
@@ -73,11 +75,13 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.storage.core.AuthToken;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Connection;
 import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Query;
 import com.redhat.thermostat.storage.core.Remove;
+import com.redhat.thermostat.storage.core.SecureStorage;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.StorageException;
 import com.redhat.thermostat.storage.core.Update;
@@ -89,7 +93,7 @@
 import com.redhat.thermostat.web.common.WebRemove;
 import com.redhat.thermostat.web.common.WebUpdate;
 
-public class WebStorage implements Storage {
+public class WebStorage implements Storage, SecureStorage {
 
     private static class CloseableHttpEntity implements Closeable, HttpEntity {
 
@@ -259,6 +263,7 @@
     private DefaultHttpClient httpClient;
     private String username;
     private String password;
+    private SecureRandom random;
 
     public WebStorage() {
         categoryIds = new HashMap<>();
@@ -266,6 +271,7 @@
         ClientConnectionManager connManager = new ThreadSafeClientConnManager();
         DefaultHttpClient client = new DefaultHttpClient(connManager);
         httpClient = client;
+        random = new SecureRandom();
     }
 
     private void initAuthentication(DefaultHttpClient client) throws MalformedURLException {
@@ -480,4 +486,46 @@
         this.password = password;
     }
 
+    @Override
+    public AuthToken generateToken() throws StorageException {
+        byte[] token = new byte[256];
+        random.nextBytes(token);
+        NameValuePair clientTokenParam = new BasicNameValuePair("client-token", Base64.encodeBase64String(token));
+        List<NameValuePair> formparams = Arrays.asList(clientTokenParam);
+        try (CloseableHttpEntity entity = post(endpoint + "/generate-token", formparams)) {
+            byte[] authToken = EntityUtils.toByteArray(entity);
+            return new AuthToken(authToken, token);
+        } catch (IOException ex) {
+            throw new StorageException(ex);
+        }
+    }
+
+    @Override
+    public boolean verifyToken(AuthToken authToken) {
+        byte[] clientToken = authToken.getClientToken();
+        byte[] token = authToken.getToken();
+        NameValuePair clientTokenParam = new BasicNameValuePair("client-token", Base64.encodeBase64String(clientToken));
+        NameValuePair tokenParam = new BasicNameValuePair("token", Base64.encodeBase64String(token));
+        List<NameValuePair> formparams = Arrays.asList(clientTokenParam, tokenParam);
+        HttpResponse response = null;
+        try {
+            HttpEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8");
+            HttpPost httpPost = new HttpPost(endpoint + "/verify-token");
+            httpPost.setEntity(entity);
+            response = httpClient.execute(httpPost);
+            StatusLine status = response.getStatusLine();
+            return status.getStatusCode() == 200;
+        } catch (IOException ex) {
+            throw new StorageException(ex);
+        } finally {
+            if (response != null) {
+                try {
+                    EntityUtils.consume(response.getEntity());
+                } catch (IOException ex) {
+                    throw new StorageException(ex);
+                }
+            }
+        }
+    }
+
 }
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java	Mon Dec 10 14:30:36 2012 +0100
@@ -52,6 +52,7 @@
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -63,6 +64,7 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.handler.AbstractHandler;
@@ -76,6 +78,8 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonParser;
 import com.google.gson.JsonSyntaxException;
+import com.redhat.thermostat.storage.core.AuthToken;
+import com.redhat.thermostat.storage.core.AuthToken;
 import com.redhat.thermostat.storage.core.Categories;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
@@ -103,6 +107,7 @@
     private Map<String,String> headers;
     private String method;
     private String requestURI;
+    private int responseStatus;
 
     private static Category category;
     private static Key<String> key1;
@@ -140,6 +145,7 @@
         headers = new HashMap<>();
         requestURI = null;
         method = null;
+        responseStatus = HttpServletResponse.SC_OK;
         registerCategory();
     }
 
@@ -172,7 +178,7 @@
                 }
                 requestBody = body.toString();
                 // Send response body.
-                response.setStatus(HttpServletResponse.SC_OK);
+                response.setStatus(responseStatus);
                 if (responseBody != null) {
                     response.getWriter().write(responseBody);
                 }
@@ -322,7 +328,6 @@
         StringReader reader = new StringReader(requestBody);
         BufferedReader bufRead = new BufferedReader(reader);
         String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        System.err.println("line: " + line);
         String[] parts = line.split("=");
         assertEquals("remove", parts[0]);
         WebRemove actualRemove = gson.fromJson(parts[1], WebRemove.class);
@@ -469,4 +474,70 @@
         assertEquals("POST", method);
         assertTrue(requestURI.endsWith("/purge"));
     }
+
+    @Test
+    public void testGenerateToken() throws UnsupportedEncodingException {
+        responseBody = "flufftoken";
+
+        AuthToken authToken = storage.generateToken();
+
+        assertTrue(requestURI.endsWith("/generate-token"));
+        assertEquals("POST", method);
+
+        String[] requestParts = requestBody.split("=");
+        assertEquals("client-token", requestParts[0]);
+        String clientTokenParam = URLDecoder.decode(requestParts[1], "UTF-8");
+        byte[] clientToken = Base64.decodeBase64(clientTokenParam);
+        assertEquals(256, clientToken.length);
+
+        assertTrue(authToken instanceof AuthToken);
+        AuthToken token = (AuthToken) authToken;
+        byte[] tokenBytes = token.getToken();
+        assertEquals("flufftoken", new String(tokenBytes));
+
+        assertTrue(Arrays.equals(clientToken, token.getClientToken()));
+
+        // Send another request and verify that we send a different client-token every time.
+        storage.generateToken();
+
+        requestParts = requestBody.split("=");
+        assertEquals("client-token", requestParts[0]);
+        clientTokenParam = URLDecoder.decode(requestParts[1], "UTF-8");
+        byte[] clientToken2 = Base64.decodeBase64(clientTokenParam);
+        assertFalse(Arrays.equals(clientToken, clientToken2));
+
+    }
+
+    @Test
+    public void testVerifyToken() throws UnsupportedEncodingException {
+        byte[] token = "stuff".getBytes();
+        byte[] clientToken = "fluff".getBytes();
+        AuthToken authToken = new AuthToken(token, clientToken);
+
+        responseStatus = 200;
+        boolean ok = storage.verifyToken(authToken);
+        assertTrue(requestURI.endsWith("/verify-token"));
+        assertEquals("POST", method);
+        String[] requestParts = requestBody.split("&");
+        assertEquals(2, requestParts.length);
+        String[] clientTokenParts = requestParts[0].split("=");
+        assertEquals(2, clientTokenParts.length);
+        assertEquals("client-token", clientTokenParts[0]);
+        String urlDecoded = URLDecoder.decode(clientTokenParts[1], "UTF-8");
+        String base64decoded = new String(Base64.decodeBase64(urlDecoded));
+        assertEquals("fluff", base64decoded);
+        String[] authTokenParts = requestParts[1].split("=");
+        assertEquals(2, authTokenParts.length);
+        assertEquals("token", authTokenParts[0]);
+        urlDecoded = URLDecoder.decode(authTokenParts[1], "UTF-8");
+        base64decoded = new String(Base64.decodeBase64(urlDecoded));
+        assertEquals("stuff", base64decoded);
+        assertTrue(ok);
+
+        // Try another one in which verification fails.
+        responseStatus = 401;
+        ok = storage.verifyToken(authToken);
+        assertFalse(ok);
+        
+    }
 }
--- a/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java	Mon Dec 10 14:30:36 2012 +0100
@@ -107,7 +107,7 @@
         ConstraintMapping constraintMap = new ConstraintMapping();
         Constraint constraint = new Constraint();
         constraint.setAuthenticate(true);
-        constraint.setRoles(new String[] { "thermostat-client", "thermostat-agent" });
+        constraint.setRoles(new String[] { "thermostat-client", "thermostat-agent", "thermostat-cmd-channel" });
         constraint.setName("Entire Application");
         constraintMap.setPathSpec("/*");
         constraintMap.setConstraint(constraint);
@@ -121,12 +121,12 @@
             
             @Override
             protected void loadUsers() throws IOException {
-                putUser("thermostat", new Password("thermostat"), new String[] { "thermostat-agent", "thermostat-client" });
+                putUser("thermostat", new Password("thermostat"), new String[] { "thermostat-agent", "thermostat-client", "thermostat-cmd-channel" });
             }
 
             @Override
             protected UserIdentity loadUser(String username) {
-                return new DefaultUserIdentity(null, null, new String[] { "thermostat-agent", "thermostat-client" });
+                return new DefaultUserIdentity(null, null, new String[] { "thermostat-agent", "thermostat-client", "thermostat-cmd-channel" });
             }
         });
         ctx.setSecurityHandler(secHandler);
--- a/web/server/pom.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/server/pom.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -105,6 +105,11 @@
       <artifactId>commons-io</artifactId>
       <version>${commons-io.version}</version>
     </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>${commons-codec.version}</version>
+    </dependency>
 
     <dependency>
       <groupId>org.osgi</groupId>
@@ -117,7 +122,6 @@
       <artifactId>thermostat-web-common</artifactId>
       <version>${project.version}</version>
     </dependency>
-
   </dependencies>
 
   <build>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/TokenManager.java	Mon Dec 10 14:30:36 2012 +0100
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2012 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.web.server;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+class TokenManager {
+
+    private static final int TOKEN_LENGTH = 256;
+
+    private SecureRandom random = new SecureRandom();
+
+    private Map<String,byte[]> tokens = Collections.synchronizedMap(new HashMap<String,byte[]>());
+
+    // Maybe use a ScheduledExecutorService if this turns out to not scale well enough.
+    private Timer timer = new Timer();
+
+    private int timeout = 30 * 1000;
+
+    void setTimeout(int timeout) {
+        this.timeout = timeout;
+    }
+
+    byte[] generateToken(String clientToken) {
+        byte[] token = new byte[TOKEN_LENGTH];
+        random.nextBytes(token);
+        tokens.put(clientToken, token);
+        scheduleRemoval(clientToken);
+        return token;
+    }
+
+    private void scheduleRemoval(final String clientToken) {
+        TimerTask task = new TimerTask() {
+            
+            @Override
+            public void run() {
+                tokens.remove(clientToken);
+            }
+        };
+        timer.schedule(task, timeout);
+    }
+
+    boolean verifyToken(String clientToken, byte[] token) {
+        if (tokens.containsKey(clientToken)) {
+            byte[] storedToken = tokens.get(clientToken);
+            boolean verified = Arrays.equals(token, storedToken);
+            if (verified) {
+                tokens.remove(clientToken);
+            }
+            return verified;
+        }
+        return false;
+    }
+
+}
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Mon Dec 10 14:30:36 2012 +0100
@@ -50,6 +50,7 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.fileupload.FileItem;
 import org.apache.commons.fileupload.FileItemFactory;
 import org.apache.commons.fileupload.FileUploadException;
@@ -65,10 +66,10 @@
 import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.model.AgentIdPojo;
 import com.redhat.thermostat.storage.model.Pojo;
 import com.redhat.thermostat.web.common.Qualifier;
@@ -82,7 +83,12 @@
 @SuppressWarnings("serial")
 public class WebStorageEndPoint extends HttpServlet {
 
+    private static final String TOKEN_MANAGER_TIMEOUT_PARAM = "token-manager-timeout";
+    private static final String TOKEN_MANAGER_KEY = "token-manager";
     private static final String ROLE_THERMOSTAT_AGENT = "thermostat-agent";
+    private static final String ROLE_THERMOSTAT_CLIENT = "thermostat-client";
+    private static final String ROLE_CMD_CHANNEL = "thermostat-cmd-channel";
+
     private Storage storage;
     private Gson gson;
 
@@ -98,6 +104,12 @@
         gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter()).create();
         categoryIds = new HashMap<>();
         categories = new HashMap<>();
+        TokenManager tokenManager = new TokenManager();
+        String timeoutParam = getInitParameter(TOKEN_MANAGER_TIMEOUT_PARAM);
+        if (timeoutParam != null) {
+            tokenManager.setTimeout(Integer.parseInt(timeoutParam));
+        }
+        getServletContext().setAttribute(TOKEN_MANAGER_KEY, tokenManager);
     }
 
     @Override
@@ -132,6 +144,10 @@
             purge(req, resp);
         } else if (cmd.equals("ping")) {
             ping(req, resp);
+        } else if (cmd.equals("generate-token")) {
+            generateToken(req, resp);
+        } else if (cmd.equals("verify-token")) {
+            verifyToken(req, resp);
         }
     }
 
@@ -356,4 +372,36 @@
         resp.flushBuffer();
     }
 
+    private void generateToken(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (! req.isUserInRole(ROLE_CMD_CHANNEL)) {
+            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+        TokenManager tokenManager = (TokenManager) getServletContext().getAttribute(TOKEN_MANAGER_KEY);
+        assert tokenManager != null;
+        String clientToken = req.getParameter("client-token");
+        byte[] token = tokenManager.generateToken(clientToken);
+        resp.setContentType("application/octet-stream");
+        resp.setContentLength(token.length);
+        resp.getOutputStream().write(token);
+    }
+
+    private void verifyToken(HttpServletRequest req, HttpServletResponse resp) {
+        if (! req.isUserInRole(ROLE_CMD_CHANNEL)) {
+            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+        TokenManager tokenManager = (TokenManager) getServletContext().getAttribute(TOKEN_MANAGER_KEY);
+        assert tokenManager != null;
+        String clientToken = req.getParameter("client-token");
+        byte[] token = Base64.decodeBase64(req.getParameter("token"));
+        boolean verified = tokenManager.verifyToken(clientToken, token);
+        if (! verified) {
+            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        } else {
+            resp.setStatus(HttpServletResponse.SC_OK);
+        }
+    }
+
+
 }
--- a/web/server/src/main/webapp/WEB-INF/web.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/server/src/main/webapp/WEB-INF/web.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -8,6 +8,11 @@
   <servlet>
     <servlet-name>reststorage-servlet</servlet-name>
     <servlet-class>com.redhat.thermostat.web.server.WebStorageEndPoint</servlet-class>
+    <!-- The timeout of the token manager in ms. We use 500ms for testing. -->
+    <init-param>
+      <param-name>token-manager-timeout</param-name>
+      <param-value>500</param-value>
+    </init-param>
   </servlet>
 
   <servlet-mapping>
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Mon Dec 10 14:30:36 2012 +0100
@@ -59,6 +59,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jetty.security.DefaultUserIdentity;
 import org.eclipse.jetty.security.MappedLoginService;
 import org.eclipse.jetty.server.Server;
@@ -177,10 +178,15 @@
             @Override
             protected void loadUsers() throws IOException {
                 putUser("testname", new Password("testpasswd"), new String[] { "thermostat-agent" });
+                putUser("test-no-role", new Password("testpasswd"), new String[] { "fluff" });
+                putUser("test-cmd-channel", new Password("testpasswd"), new String[] { "thermostat-cmd-channel" });
             }
             
             @Override
             protected UserIdentity loadUser(String username) {
+                if (username.equals("test-cmd-channel")) {
+                    return new DefaultUserIdentity(null, null, new String[] { "thermostat-cmd-channel" });
+                }
                 return new DefaultUserIdentity(null, null, new String[] { "thermostat-agent" });
             }
         });
@@ -289,10 +295,7 @@
         URL url = new URL(endpoint + "/put-pojo");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        BASE64Encoder enc = new BASE64Encoder();
-        String userpassword = "testname:testpasswd";
-        String encodedAuthorization = enc.encode( userpassword.getBytes() );
-        conn.setRequestProperty("Authorization", "Basic "+ encodedAuthorization);
+        sendAuthorization(conn, "testname", "testpasswd");
 
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
@@ -310,6 +313,13 @@
         verify(mockStorage).putPojo(category, true, expected1);
     }
 
+    private void sendAuthorization(HttpURLConnection conn, String username, String passwd) {
+        BASE64Encoder enc = new BASE64Encoder();
+        String userpassword = username + ":" + passwd;
+        String encodedAuthorization = enc.encode( userpassword.getBytes() );
+        conn.setRequestProperty("Authorization", "Basic "+ encodedAuthorization);
+    }
+
     @Test
     public void testRemovePojo() throws IOException {
 
@@ -423,7 +433,6 @@
         out.write("--fluff--\r\n");
         out.flush();
         int status = conn.getResponseCode();
-        System.err.println("status: " + status);
         ArgumentCaptor<InputStream> inCaptor = ArgumentCaptor.forClass(InputStream.class);
         verify(mockStorage).saveFile(eq("fluff"), inCaptor.capture());
         InputStream in = inCaptor.getValue();
@@ -510,6 +519,118 @@
     }
 
     private String getEndpoint() {
-        return "http://testname:testpasswd@localhost:" + port + "/storage";
+        return "http://localhost:" + port + "/storage";
+    }
+
+    @Test
+    public void testBasicGenerateToken() throws IOException {
+        
+        verifyGenerateToken();
+    }
+
+    @Test
+    public void testGenerateTokenWithoutAuth() throws IOException {
+        
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/generate-token");
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        sendAuthorization(conn, "test-no-role", "testpasswd");
+        conn.setDoOutput(true);
+        conn.setDoInput(true);
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("client-token=fluff");
+        out.flush();
+        assertEquals(401, conn.getResponseCode());
+    }
+
+    @Test
+    public void testBasicGenerateVerifyToken() throws IOException {
+        
+        byte[] token = verifyGenerateToken();
+
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/verify-token");
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
+        sendAuthorization(conn, "test-cmd-channel", "testpasswd");
+        conn.setDoOutput(true);
+        conn.setDoInput(true);
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("client-token=fluff&token=" + URLEncoder.encode(Base64.encodeBase64String(token), "UTF-8"));
+        out.flush();
+        assertEquals(200, conn.getResponseCode());
+    }
+
+    @Test
+    public void testTokenTimeout() throws IOException, InterruptedException {
+        
+        byte[] token = verifyGenerateToken();
+
+        Thread.sleep(700); // Timeout is set to 500ms for tests, 700ms should be enough for everybody. ;-)
+
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/verify-token");
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
+        sendAuthorization(conn, "test-cmd-channel", "testpasswd");
+        conn.setDoOutput(true);
+        conn.setDoInput(true);
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("client-token=fluff&token=" + URLEncoder.encode(Base64.encodeBase64String(token), "UTF-8"));
+        out.flush();
+        assertEquals(401, conn.getResponseCode());
+    }
+
+    @Test
+    public void testVerifyNonExistentToken() throws IOException {
+        
+        byte[] token = "fluff".getBytes();
+
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/verify-token");
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
+        sendAuthorization(conn, "test-cmd-channel", "testpasswd");
+        conn.setDoOutput(true);
+        conn.setDoInput(true);
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("client-token=fluff&token=" + URLEncoder.encode(Base64.encodeBase64String(token), "UTF-8"));
+        out.flush();
+        assertEquals(401, conn.getResponseCode());
+    }
+
+    private byte[] verifyGenerateToken() throws IOException {
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/generate-token");
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        sendAuthorization(conn, "test-cmd-channel", "testpasswd");
+        conn.setDoOutput(true);
+        conn.setDoInput(true);
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("client-token=fluff");
+        out.flush();
+        InputStream in = conn.getInputStream();
+        int length = conn.getContentLength();
+        byte[] token  = new byte[length];
+        assertEquals(256, length);
+        int totalRead = 0;
+        while (totalRead < length) {
+            int read = in.read(token, totalRead, length - totalRead);
+            if (read < 0) {
+                fail();
+            }
+            totalRead += read;
+        }
+        return token;
     }
 }
--- a/web/war/src/main/webapp/WEB-INF/web.xml	Tue Dec 04 20:14:25 2012 -0500
+++ b/web/war/src/main/webapp/WEB-INF/web.xml	Mon Dec 10 14:30:36 2012 +0100
@@ -15,6 +15,11 @@
       <param-name>storage.endpoint</param-name>
       <param-value>mongodb://127.0.0.1:27518</param-value>
     </init-param>
+    <!-- The timeout of the token manager in ms -->
+    <init-param>
+      <param-name>token-manager-timeout</param-name>
+      <param-value>3000</param-value>
+    </init-param>
     <servlet-name>reststorage-servlet</servlet-name>
     <servlet-class>com.redhat.thermostat.web.server.WebStorageEndPoint</servlet-class>
   </servlet>
@@ -32,6 +37,7 @@
     <auth-constraint>
       <role-name>thermostat-agent</role-name>
       <role-name>thermostat-client</role-name>
+      <role-name>thermostat-cmd-channel</role-name>
     </auth-constraint>
   </security-constraint>