Repository: org-arl/fjage (language: python)

Issue:
Connection-based firewall
`org.arl.fjage.auth.AbstractFirewall` does not seem to take multiple connections (with different credentials) into account. The `auth ` flag in the code essentially behaves as a global variable. Playing around with this seems to show this behaviour. I might be using this wrong though.

I've made something that addresses this issue: https://github.com/ngyewch/fjage-extras/tree/main/fjage-security

Connection-based firewall
`org.arl.fjage.auth.AbstractFirewall` does not seem to take multiple connections (with different credentials) into account. The `auth ` flag in the code essentially behaves as a global variable. Playing around with this seems to show this behaviour. I might be using this wrong though.

I've made something that addresses this issue: https://github.com/ngyewch/fjage-extras/tree/main/fjage-security



Discussion / hints:

Fix patch:
diff --git a/src/main/java/org/arl/fjage/auth/AbstractFirewall.java b/src/main/java/org/arl/fjage/auth/AbstractFirewall.java
index 0265025c..17e137f4 100644
--- a/src/main/java/org/arl/fjage/auth/AbstractFirewall.java
+++ b/src/main/java/org/arl/fjage/auth/AbstractFirewall.java
@@ -17,7 +17,7 @@
 /**
  * An abstract firewall class that allows all traffic to pass through once
  * authenticated. Authentication is implemented by a class that extends this
- * class by implemeting the {@link Firewall#authenticate(Connector,String)} method.
+ * class by implementing the {@link Firewall#authenticate(String)} method.
  */
 public abstract class AbstractFirewall implements Firewall {
 
diff --git a/src/main/java/org/arl/fjage/auth/AllowAfterAuth.java b/src/main/java/org/arl/fjage/auth/AllowAfterAuth.java
index 6304aeff..076dcd0f 100644
--- a/src/main/java/org/arl/fjage/auth/AllowAfterAuth.java
+++ b/src/main/java/org/arl/fjage/auth/AllowAfterAuth.java
@@ -10,7 +10,7 @@
 
 package org.arl.fjage.auth;
 
-import org.arl.fjage.connectors.Connector;
+import java.util.function.Supplier;
 
 /**
  * A permissive firewall that allows all traffic to pass through, but requires
@@ -20,8 +20,13 @@
  */
 public class AllowAfterAuth extends AbstractFirewall {
 
+  /**
+   * AllowAfterAuth supplier.
+   */
+  public static final Supplier<Firewall> SUPPLIER = AllowAfterAuth::new;
+
   @Override
-  public boolean authenticate(Connector conn, String creds) {
+  public boolean authenticate(String creds) {
     if (creds == null) auth = false;
     else {
       log.fine("Authentication successful ["+creds+"]");
@@ -30,4 +35,9 @@ public boolean authenticate(Connector conn, String creds) {
     return auth;
   }
 
+  @Override
+  public void signoff() {
+    // do nothing
+  }
+
 }
diff --git a/src/main/java/org/arl/fjage/auth/AllowAll.java b/src/main/java/org/arl/fjage/auth/AllowAll.java
index 1453e005..180ce365 100644
--- a/src/main/java/org/arl/fjage/auth/AllowAll.java
+++ b/src/main/java/org/arl/fjage/auth/AllowAll.java
@@ -10,18 +10,39 @@
 
 package org.arl.fjage.auth;
 
-import org.arl.fjage.connectors.Connector;
+import org.arl.fjage.AgentID;
+import org.arl.fjage.remote.JsonMessage;
+
+import java.util.function.Supplier;
 
 /**
  * A permissive firewall that allows all traffic to pass through.
  */
-public class AllowAll extends AbstractFirewall {
+public class AllowAll implements Firewall {
+
+  /**
+   * AllowAll supplier.
+   */
+  public static final Supplier<Firewall> SUPPLIER = AllowAll::new;
+
+  @Override
+  public boolean authenticate(String creds) {
+    return true;
+  }
+
+  @Override
+  public boolean permit(JsonMessage rq) {
+    return true;
+  }
+
+  @Override
+  public boolean permit(AgentID aid) {
+    return true;
+  }
 
   @Override
-  public boolean authenticate(Connector conn, String creds) {
-    if (creds != null) log.fine("Authentication successful [AllowAll]");
-    auth = true;
-    return auth;
+  public void signoff() {
+    // do nothing
   }
 
 }
diff --git a/src/main/java/org/arl/fjage/auth/DenyAll.java b/src/main/java/org/arl/fjage/auth/DenyAll.java
index 3cb74216..097c09cb 100644
--- a/src/main/java/org/arl/fjage/auth/DenyAll.java
+++ b/src/main/java/org/arl/fjage/auth/DenyAll.java
@@ -10,18 +10,39 @@
 
 package org.arl.fjage.auth;
 
-import org.arl.fjage.connectors.Connector;
+import org.arl.fjage.AgentID;
+import org.arl.fjage.remote.JsonMessage;
+
+import java.util.function.Supplier;
 
 /**
  * A reluctant firewall that denies all traffic to pass through.
  */
-public class DenyAll extends AbstractFirewall {
+public class DenyAll implements Firewall {
+
+  /**
+   * DenyAll supplier.
+   */
+  public static final Supplier<Firewall> SUPPLIER = DenyAll::new;
+
+  @Override
+  public boolean authenticate(String creds) {
+    return false;
+  }
+
+  @Override
+  public boolean permit(JsonMessage rq) {
+    return false;
+  }
+
+  @Override
+  public boolean permit(AgentID aid) {
+    return false;
+  }
 
   @Override
-  public boolean authenticate(Connector conn, String creds) {
-    if (creds != null) log.fine("Authentication unsuccessful [DenyAll]");
-    auth = false;
-    return auth;
+  public void signoff() {
+    // do nothing
   }
 
 }
diff --git a/src/main/java/org/arl/fjage/auth/Firewall.java b/src/main/java/org/arl/fjage/auth/Firewall.java
index f77e4b1e..20e9ce86 100644
--- a/src/main/java/org/arl/fjage/auth/Firewall.java
+++ b/src/main/java/org/arl/fjage/auth/Firewall.java
@@ -11,26 +11,23 @@
 package org.arl.fjage.auth;
 
 import org.arl.fjage.AgentID;
-import org.arl.fjage.connectors.Connector;
 import org.arl.fjage.remote.JsonMessage;
 
 /**
  * Firewall interface for API access to fjage master containers.
+ *
+ * Each connection to a master container should and will be assigned its own <code>Firewall</code> instance.
+ * <code>Firewall</code> instances should be considered stateful.
  */
 public interface Firewall {
 
   /**
    * Authenticates peer using specified credentials.
-   * <p>
-   * When a firewall is first bound to a connection handler, this method is
-   * called with null credentials to bind a connector to the firewall. The
-   * method is also called with null credentials when a connection is closed.
    *
-   * @param conn connection to peer.
    * @param creds credentials, or null if logging out.
    * @return true if authenticated successfully, false otherwise.
    */
-  public boolean authenticate(Connector conn, String creds);
+  public boolean authenticate(String creds);
 
   /**
    * Checks whether a JSON message can be accepted over this connection.
@@ -49,4 +46,10 @@ public interface Firewall {
    */
   public boolean permit(AgentID aid);
 
+  /**
+   * Called when the connection is closed.
+   * The <code>Firewall</code> instance should perform cleanup.
+   */
+  public void signoff();
+
 }
diff --git a/src/main/java/org/arl/fjage/remote/Action.java b/src/main/java/org/arl/fjage/remote/Action.java
index 8ca4dadf..cc532565 100644
--- a/src/main/java/org/arl/fjage/remote/Action.java
+++ b/src/main/java/org/arl/fjage/remote/Action.java
@@ -15,7 +15,7 @@
 /**
  * JSON message actions.
  */
-enum Action {
+public enum Action {
   @SerializedName("auth")             AUTH,
   @SerializedName("agents")           AGENTS,
   @SerializedName("containsAgent")    CONTAINS_AGENT,
diff --git a/src/main/java/org/arl/fjage/remote/ConnectionHandler.java b/src/main/java/org/arl/fjage/remote/ConnectionHandler.java
index 818743f0..1b5dd8d2 100644
--- a/src/main/java/org/arl/fjage/remote/ConnectionHandler.java
+++ b/src/main/java/org/arl/fjage/remote/ConnectionHandler.java
@@ -95,7 +95,6 @@ public void run() {
         println(ALIVE);
       }
     }
-    fw.authenticate(conn, null);
     while (conn != null) {
       String s = null;
       try {
@@ -144,7 +143,7 @@ public void run() {
         } else {
           // new request
           if (rq.action == Action.AUTH) {
-            boolean b = fw.authenticate(conn, rq.creds);
+            boolean b = fw.authenticate(rq.creds);
             respondAuth(rq, b);
           }
           else if (fw.permit(rq)) pool.execute(new RemoteTask(rq));
@@ -154,7 +153,7 @@ public void run() {
         log.warning("Bad JSON request: "+ex.toString() + " in " + s);
       }
     }
-    fw.authenticate(conn, null);
+    fw.signoff();
     close();
     pool.shutdown();
   }
diff --git a/src/main/java/org/arl/fjage/remote/MasterContainer.java b/src/main/java/org/arl/fjage/remote/MasterContainer.java
index c859b0e1..5af7512f 100644
--- a/src/main/java/org/arl/fjage/remote/MasterContainer.java
+++ b/src/main/java/org/arl/fjage/remote/MasterContainer.java
@@ -14,6 +14,7 @@
 import org.arl.fjage.*;
 import org.arl.fjage.auth.*;
 import org.arl.fjage.connectors.*;
+import java.util.function.Supplier;
 
 /**
  * Master container supporting multiple remote slave containers. Agents in linked
@@ -32,7 +33,7 @@ public class MasterContainer extends RemoteContainer implements ConnectionListen
   private TcpServer listener = null;
   private List<ConnectionHandler> slaves = new ArrayList<ConnectionHandler>();
   private boolean needsCleanup = false;
-  private Firewall fw = new AllowAll();
+  private Supplier<Firewall> fwSupplier = AllowAll.SUPPLIER;
 
   ////////////// Constructors
 
@@ -48,14 +49,14 @@ public MasterContainer(Platform platform) {
 
   /**
    * Creates a master container, runs its TCP server on an automatically selected port,
-   * with a specified firewall.
+   * with a specified firewall supplier.
    *
    * @param platform platform on which the container runs.
-   * @param fw firewall to use.
+   * @param fwSupplier firewall supplier to use.
    */
-  public MasterContainer(Platform platform, Firewall fw) {
+  public MasterContainer(Platform platform, Supplier<Firewall> fwSupplier) {
     super(platform);
-    this.fw = fw;
+    this.fwSupplier = fwSupplier;
     openTcpServer(0);
   }
 
@@ -72,15 +73,15 @@ public MasterContainer(Platform platform, int port) {
 
   /**
    * Creates a master container, runs its TCP server on a specified port,
-   * with a specified firewall.
+   * with a specified firewall supplier.
    *
    * @param platform platform on which the container runs.
    * @param port port on which the container's TCP server runs.
-   * @param fw firewall to use.
+   * @param fwSupplier firewall supplier to use.
    */
-  public MasterContainer(Platform platform, int port, Firewall fw) {
+  public MasterContainer(Platform platform, int port, Supplier<Firewall> fwSupplier) {
     super(platform);
-    this.fw = fw;
+    this.fwSupplier = fwSupplier;
     openTcpServer(port);
   }
 
@@ -97,15 +98,15 @@ public MasterContainer(Platform platform, String name) {
 
   /**
    * Creates a named master container, runs its TCP server on an automatically selected port,
-   * with a specified firewall.
+   * with a specified firewall supplier.
    *
    * @param platform platform on which the container runs.
    * @param name name of the container.
-   * @param fw firewall to use.
+   * @param fwSupplier firewall supplier to use.
    */
-  public MasterContainer(Platform platform, String name, Firewall fw) {
+  public MasterContainer(Platform platform, String name, Supplier<Firewall> fwSupplier) {
     super(platform, name);
-    this.fw = fw;
+    this.fwSupplier = fwSupplier;
     openTcpServer(0);
   }
 
@@ -123,16 +124,16 @@ public MasterContainer(Platform platform, String name, int port) {
 
   /**
    * Creates a named master container, runs its TCP server on a specified port,
-   * with a specified firewall.
+   * with a specified firewall supplier.
    *
    * @param platform platform on which the container runs.
    * @param name of the container.
    * @param port port on which the container's TCP server runs.
-   * @param fw firewall to use.
+   * @param fwSupplier firewall supplier to use.
    */
-  public MasterContainer(Platform platform, String name, int port, Firewall fw) {
+  public MasterContainer(Platform platform, String name, int port, Supplier<Firewall> fwSupplier) {
     super(platform, name);
-    this.fw = fw;
+    this.fwSupplier = fwSupplier;
     openTcpServer(port);
   }
 
@@ -148,13 +149,13 @@ public int getPort() {
 
   /**
    * Adds a connector over which the master container listens. The connector uses
-   * any default firewall configured for the container.
+   * a firewall acquired from the firewall supplier configured for the container.
    *
    * @param conn connector.
    */
   public void addConnector(Connector conn) {
     log.info("Listening on "+conn.getName());
-    ConnectionHandler t = new ConnectionHandler(conn, MasterContainer.this, fw);
+    ConnectionHandler t = new ConnectionHandler(conn, MasterContainer.this, fwSupplier.get());
     synchronized(slaves) {
       slaves.add(t);
     }
@@ -405,7 +406,7 @@ public void connectionClosed(ConnectionHandler handler) {
   @Override
   public void connected(Connector conn) {
     log.info("Incoming connection "+conn.toString());
-    ConnectionHandler t = new ConnectionHandler(conn, MasterContainer.this, fw);
+    ConnectionHandler t = new ConnectionHandler(conn, MasterContainer.this, fwSupplier.get());
     synchronized(slaves) {
       slaves.add(t);
     }


Test patch:
diff --git a/src/test/java/org/arl/fjage/test/BasicTests.java b/src/test/java/org/arl/fjage/test/BasicTests.java
index fb5aadcb..f91c744e 100644
--- a/src/test/java/org/arl/fjage/test/BasicTests.java
+++ b/src/test/java/org/arl/fjage/test/BasicTests.java
@@ -17,7 +17,6 @@
 import java.util.Random;
 import java.util.logging.Logger;
 import org.arl.fjage.*;
-import org.arl.fjage.auth.*;
 import org.arl.fjage.param.*;
 import org.arl.fjage.persistence.Store;
 import org.arl.fjage.remote.Gateway;
@@ -181,106 +180,6 @@ public void testGateway() throws IOException {
     platform.shutdown();
   }
 
-  @Test
-  public void testFirewall1() throws IOException {
-    log.info("testFirewall1");
-    Platform platform = new RealTimePlatform();
-    MasterContainer master = new MasterContainer(platform, new AllowAll());
-    ServerAgent server = new ServerAgent();
-    master.add("S", server);
-    platform.start();
-    Gateway gw = new Gateway("localhost", master.getPort());
-    AgentID s = gw.agentForService("server");
-    assertNotNull(s);
-    Message req = new RequestMessage(server.getAgentID());
-    Message rsp = gw.request(req, 1000);
-    assertNotNull(rsp);
-    gw.close();
-    platform.shutdown();
-    assertEquals(1, server.requests);
-  }
-
-  @Test
-  public void testFirewall2() throws IOException {
-    log.info("testFirewall2");
-    Platform platform = new RealTimePlatform();
-    MasterContainer master = new MasterContainer(platform, new DenyAll());
-    ServerAgent server = new ServerAgent();
-    master.add("S", server);
-    platform.start();
-    Gateway gw = new Gateway("localhost", master.getPort());
-    try {
-      gw.agentForService("server");
-      fail("Should have thrown AuthFailureException");
-    } catch (AuthFailureException ex) {
-      // all good
-    }
-    Message req = new RequestMessage(server.getAgentID());
-    try {
-      gw.request(req, 1000);
-      fail("Should have thrown AuthFailureException");
-    } catch (AuthFailureException ex) {
-      // all good
-    }
-    gw.close();
-    platform.shutdown();
-    assertEquals(0, server.requests);
-  }
-
-  @Test
-  public void testFirewall3() throws IOException {
-    log.info("testFirewall3");
-    Platform platform = new RealTimePlatform();
-    MasterContainer master = new MasterContainer(platform, new AllowAfterAuth());
-    ServerAgent server = new ServerAgent();
-    master.add("S", server);
-    platform.start();
-    while (!master.isRunning()) {
-      try {
-        Thread.sleep(100);
-      } catch (InterruptedException ex) {
-        // ignore
-      }
-    }
-    Gateway gw = new Gateway("localhost", master.getPort());
-    try {
-      gw.agentForService("server");
-      fail("Should have thrown AuthFailureException");
-    } catch (AuthFailureException ex) {
-      // all good
-    }
-    gw.authenticate("somecreds");
-    AgentID s = gw.agentForService("server");
-    assertNotNull(s);
-    Message req = new RequestMessage(server.getAgentID());
-    Message rsp = gw.request(req, 1000);
-    assertNotNull(rsp);
-    gw.close();
-    platform.shutdown();
-    assertEquals(1, server.requests);
-  }
-
-  @Test
-  public void testFirewall4() throws IOException {
-    log.info("testFirewall4");
-    Platform platform = new RealTimePlatform();
-    MasterContainer master = new MasterContainer(platform, new AllowAfterAuth());
-    platform.start();
-    Gateway gw = new Gateway("localhost", master.getPort());
-    gw.subscribe(new AgentID("test1", true));
-    master.send(new NuisanceMessage(new AgentID("test1", true)));
-    Message msg = gw.receive(1000);
-    assertNull(msg);
-    gw.authenticate("somecreds");
-    gw.subscribe(new AgentID("test1", true));
-    master.send(new NuisanceMessage(new AgentID("test1", true)));
-    msg = gw.receive(1000);
-    assertNotNull(msg);
-    assertSame(msg.getClass(), NuisanceMessage.class);
-    gw.close();
-    platform.shutdown();
-  }
-
   @Test
   public void testFSM() {
     log.info("testFSM");
diff --git a/src/test/java/org/arl/fjage/test/FirewallTests.java b/src/test/java/org/arl/fjage/test/FirewallTests.java
new file mode 100644
index 00000000..87680c38
--- /dev/null
+++ b/src/test/java/org/arl/fjage/test/FirewallTests.java
@@ -0,0 +1,304 @@
+/******************************************************************************
+
+Copyright (c) 2013, Mandar Chitre
+
+This file is part of fjage which is released under Simplified BSD License.
+See file LICENSE.txt or go to http://www.opensource.org/licenses/BSD-3-Clause
+for full license details.
+
+******************************************************************************/
+
+package org.arl.fjage.test;
+
+import org.arl.fjage.*;
+import org.arl.fjage.auth.*;
+import org.arl.fjage.param.Parameter;
+import org.arl.fjage.remote.Gateway;
+import org.arl.fjage.remote.MasterContainer;
+import org.arl.fjage.test.auth.SimpleFirewallSupplier;
+import org.arl.fjage.test.auth.TracedFirewallSupplier;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.*;
+
+public class FirewallTests {
+
+  @Rule
+  public TestName testName = new TestName();
+  private final Logger log = Logger.getLogger(getClass().getName());
+
+  @Before
+  public void beforeTesting() {
+    LogFormatter.install(null);
+    log.info(String.format("==== BEGIN %s ====", testName.getMethodName()));
+  }
+
+  @After
+  public void afterTesting() {
+    log.info(String.format("==== END %s ====", testName.getMethodName()));
+  }
+
+  @Test
+  public void testFirewall1() throws IOException {
+    Platform platform = new RealTimePlatform();
+    MasterContainer master = new MasterContainer(platform, AllowAll.SUPPLIER);
+    ServerAgent server = new ServerAgent();
+    master.add("S", server);
+    platform.start();
+    Gateway gw = new Gateway("localhost", master.getPort());
+    AgentID s = gw.agentForService("server");
+    assertNotNull(s);
+    Message req = new RequestMessage(server.getAgentID());
+    Message rsp = gw.request(req, 1000);
+    assertNotNull(rsp);
+    gw.close();
+    platform.shutdown();
+    assertEquals(1, server.requests);
+  }
+
+  @Test
+  public void testFirewall2() throws IOException {
+    Platform platform = new RealTimePlatform();
+    MasterContainer master = new MasterContainer(platform, DenyAll.SUPPLIER);
+    ServerAgent server = new ServerAgent();
+    master.add("S", server);
+    platform.start();
+    Gateway gw = new Gateway("localhost", master.getPort());
+    try {
+      gw.agentForService("server");
+      fail("Should have thrown AuthFailureException");
+    } catch (AuthFailureException ex) {
+      // all good
+    }
+    Message req = new RequestMessage(server.getAgentID());
+    try {
+      gw.request(req, 1000);
+      fail("Should have thrown AuthFailureException");
+    } catch (AuthFailureException ex) {
+      // all good
+    }
+    gw.close();
+    platform.shutdown();
+    assertEquals(0, server.requests);
+  }
+
+  @Test
+  public void testFirewall3() throws IOException {
+    Platform platform = new RealTimePlatform();
+    MasterContainer master = new MasterContainer(platform, AllowAfterAuth.SUPPLIER);
+    ServerAgent server = new ServerAgent();
+    master.add("S", server);
+    platform.start();
+    while (!master.isRunning()) {
+      try {
+        Thread.sleep(100);
+      } catch (InterruptedException ex) {
+        // ignore
+      }
+    }
+    Gateway gw = new Gateway("localhost", master.getPort());
+    try {
+      gw.agentForService("server");
+      fail("Should have thrown AuthFailureException");
+    } catch (AuthFailureException ex) {
+      // all good
+    }
+    gw.authenticate("somecreds");
+    AgentID s = gw.agentForService("server");
+    assertNotNull(s);
+    Message req = new RequestMessage(server.getAgentID());
+    Message rsp = gw.request(req, 1000);
+    assertNotNull(rsp);
+    gw.close();
+    platform.shutdown();
+    assertEquals(1, server.requests);
+  }
+
+  @Test
+  public void testFirewall4() throws IOException {
+    Platform platform = new RealTimePlatform();
+    MasterContainer master = new MasterContainer(platform, AllowAfterAuth.SUPPLIER);
+    platform.start();
+    Gateway gw = new Gateway("localhost", master.getPort());
+    gw.subscribe(new AgentID("test1", true));
+    master.send(new NuisanceMessage(new AgentID("test1", true)));
+    Message msg = gw.receive(1000);
+    assertNull(msg);
+    gw.authenticate("somecreds");
+    gw.subscribe(new AgentID("test1", true));
+    master.send(new NuisanceMessage(new AgentID("test1", true)));
+    msg = gw.receive(1000);
+    assertNotNull(msg);
+    assertSame(msg.getClass(), NuisanceMessage.class);
+    gw.close();
+    platform.shutdown();
+  }
+
+  @Test
+  public void testSimpleFirewall1() throws IOException {
+    final SimpleFirewallSupplier simpleFirewallSupplier = new SimpleFirewallSupplier()
+        .addUserConfiguration("somecreds", userConfiguration -> userConfiguration
+            .allowedServiceNames("server")
+            .allowedAgentNames("server1")
+            .allowedTopicNames("server1__ntf")
+        );
+    final Supplier<Firewall> fwSupplier = new TracedFirewallSupplier(simpleFirewallSupplier);
+
+    final Platform platform = new RealTimePlatform();
+    final MasterContainer master = new MasterContainer(platform, fwSupplier);
+
+    final ServerAgent serverAgent1 = new ServerAgent();
+    master.add("server1", serverAgent1);
+
+    final ServerAgent2 serverAgent2 = new ServerAgent2();
+    master.add("server2", serverAgent2);
+
+    platform.start();
+
+    while (!master.isRunning()) {
+      try {
+        Thread.sleep(100);
+      } catch (InterruptedException ex) {
+        // ignore
+      }
+    }
+
+    final Gateway gw = new Gateway("localhost", master.getPort());
+
+    try {
+      gw.agentForService("server");
+      fail("Should have thrown AuthFailureException");
+    } catch (AuthFailureException ex) {
+      // all good
+    }
+
+    gw.authenticate("somecreds");
+
+    {
+      final AgentID serverAgentId = gw.agentForService("server");
+      assertNotNull(serverAgentId);
+      gw.subscribe(gw.topic(serverAgentId));
+      final Message req = new RequestMessage(serverAgentId);
+      final Message rsp = gw.request(req, 1000);
+      assertNotNull(rsp);
+    }
+
+    try {
+      final AgentID server2AgentId = gw.agentForService("server2");
+      fail("Should have thrown AuthFailureException");
+    } catch (AuthFailureException ex) {
+      // all good
+    }
+
+    {
+      final Message msg = gw.receive(1000);
+      assertNotNull(msg);
+    }
+
+    while (!master.isRunning()) {
+      try {
+        Thread.sleep(2000);
+      } catch (InterruptedException ex) {
+        // ignore
+      }
+    }
+
+    gw.close();
+    platform.shutdown();
+    assertEquals(1, serverAgent1.requests);
+  }
+
+  private static class RequestMessage extends Message {
+    private static final long serialVersionUID = 1L;
+    public int x;
+
+    public RequestMessage(AgentID recipient) {
+      super(recipient, Performative.REQUEST);
+    }
+  }
+
+  private static class ResponseMessage extends Message {
+    private static final long serialVersionUID = 1L;
+    public int x, y;
+
+    public ResponseMessage(Message request) {
+      super(request, Performative.INFORM);
+    }
+  }
+
+  private static class NuisanceMessage extends Message {
+    private static final long serialVersionUID = 1L;
+
+    public NuisanceMessage(AgentID recipient) {
+      super(recipient, Performative.INFORM);
+    }
+  }
+
+  public enum Params implements Parameter {
+    x, y, s
+  }
+
+  private static class ServerAgent extends Agent {
+    public int requests = 0, nuisance = 0;
+
+    @Override
+    public void init() {
+      register("server");
+      subscribe(topic("noise"));
+      add(new MessageBehavior(RequestMessage.class) {
+        @Override
+        public void onReceive(Message msg) {
+          requests++;
+          RequestMessage req = (RequestMessage) msg;
+          ResponseMessage rsp = new ResponseMessage(req);
+          rsp.x = req.x;
+          rsp.y = 2 * req.x + 1;
+          agent.send(rsp);
+        }
+      });
+      add(new MessageBehavior(NuisanceMessage.class) {
+        @Override
+        public void onReceive(Message msg) {
+          nuisance++;
+        }
+      });
+      final AgentID topic = topic();
+      add(new TickerBehavior(1000, () -> send(new NuisanceMessage(topic))));
+    }
+  }
+
+  private static class ServerAgent2 extends Agent {
+    public int requests = 0, nuisance = 0;
+
+    @Override
+    public void init() {
+      register("server2");
+      subscribe(topic("noise"));
+      add(new MessageBehavior(RequestMessage.class) {
+        @Override
+        public void onReceive(Message msg) {
+          requests++;
+          RequestMessage req = (RequestMessage) msg;
+          ResponseMessage rsp = new ResponseMessage(req);
+          rsp.x = req.x;
+          rsp.y = 2 * req.x + 1;
+          agent.send(rsp);
+        }
+      });
+      add(new MessageBehavior(NuisanceMessage.class) {
+        @Override
+        public void onReceive(Message msg) {
+          nuisance++;
+        }
+      });
+    }
+  }
+}
diff --git a/src/test/java/org/arl/fjage/test/SimpleFirewallTests.java b/src/test/java/org/arl/fjage/test/SimpleFirewallTests.java
new file mode 100644
index 00000000..77018e7c
--- /dev/null
+++ b/src/test/java/org/arl/fjage/test/SimpleFirewallTests.java
@@ -0,0 +1,142 @@
+/******************************************************************************
+
+Copyright (c) 2013, Mandar Chitre
+
+This file is part of fjage which is released under Simplified BSD License.
+See file LICENSE.txt or go to http://www.opensource.org/licenses/BSD-3-Clause
+for full license details.
+
+******************************************************************************/
+
+package org.arl.fjage.test;
+
+import org.arl.fjage.*;
+import org.arl.fjage.auth.*;
+import org.arl.fjage.remote.Action;
+import org.arl.fjage.remote.JsonMessage;
+import org.arl.fjage.test.auth.SimpleFirewallSupplier;
+import org.junit.*;
+import org.junit.rules.TestName;
+
+import java.util.logging.Logger;
+
+public class SimpleFirewallTests {
+
+  private static final String AGENT1_NAME = "agent1";
+  private static final String AGENT2_NAME = "agent2";
+  private static final String AGENT3_NAME = "agent3";
+  private static final AgentID AGENT1 = new AgentID(AGENT1_NAME);
+  private static final AgentID AGENT2 = new AgentID(AGENT2_NAME);
+  private static final AgentID AGENT3 = new AgentID(AGENT3_NAME);
+
+  @Rule
+  public TestName testName = new TestName();
+  private final Logger log = Logger.getLogger(getClass().getName());
+  private final SimpleFirewallSupplier simpleFirewallSupplier = new SimpleFirewallSupplier()
+      .addUserConfiguration("credentials1", userConfiguration -> userConfiguration
+          .allowedAgentNames("agent1")
+      )
+      .addUserConfiguration("credentials2", userConfiguration -> userConfiguration
+          .allowedAgentNames("agent2")
+      );
+
+  @Before
+  public void beforeTesting() {
+    LogFormatter.install(null);
+    log.info(String.format("==== BEGIN %s ====", testName.getMethodName()));
+  }
+
+  @After
+  public void afterTesting() {
+    log.info(String.format("==== END %s ====", testName.getMethodName()));
+  }
+
+  @Test
+  public void distinctInstances() {
+    final Firewall fw1 = simpleFirewallSupplier.get();
+    final Firewall fw2 = simpleFirewallSupplier.get();
+    final Firewall fw3 = simpleFirewallSupplier.get();
+    Assert.assertNotEquals(fw1, fw2);
+    Assert.assertNotEquals(fw1, fw3);
+    Assert.assertNotEquals(fw2, fw3);
+  }
+
+  @Test
+  public void authentication1() {
+    final Firewall fw = simpleFirewallSupplier.get();
+
+    // fw is not authenticated
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT3)));
+
+    // authenticate fw
+    Assert.assertEquals(true, fw.authenticate("credentials1"));
+    Assert.assertEquals(true, fw.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT3)));
+  }
+
+  @Test
+  public void authentication2() {
+    final Firewall fw = simpleFirewallSupplier.get();
+
+    // fw is not authenticated
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT3)));
+
+    // authenticate fw
+    Assert.assertEquals(true, fw.authenticate("credentials2"));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(true, fw.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT3)));
+  }
+
+  @Test
+  public void badAuthentication() {
+    final Firewall fw = simpleFirewallSupplier.get();
+
+    // fw is not authenticated
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT3)));
+
+    // authenticate fw (with unknown credentials)
+    Assert.assertEquals(false, fw.authenticate("credentials0"));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw.permit(newSendJsonMessage(AGENT3)));
+  }
+
+  @Test
+  public void distinctStates() {
+    final Firewall fw1 = simpleFirewallSupplier.get();
+    Assert.assertEquals(true, fw1.authenticate("credentials1"));
+
+    final Firewall fw2 = simpleFirewallSupplier.get();
+    Assert.assertEquals(true, fw2.authenticate("credentials2"));
+
+    final Firewall fw3 = simpleFirewallSupplier.get();
+    Assert.assertEquals(false, fw3.authenticate("credentials0"));
+
+    Assert.assertEquals(true, fw1.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw1.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw1.permit(newSendJsonMessage(AGENT3)));
+
+    Assert.assertEquals(false, fw2.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(true, fw2.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw2.permit(newSendJsonMessage(AGENT3)));
+
+    Assert.assertEquals(false, fw3.permit(newSendJsonMessage(AGENT1)));
+    Assert.assertEquals(false, fw3.permit(newSendJsonMessage(AGENT2)));
+    Assert.assertEquals(false, fw3.permit(newSendJsonMessage(AGENT3)));
+  }
+
+  private JsonMessage newSendJsonMessage(AgentID recipient) {
+    final JsonMessage jsonMessage = new JsonMessage();
+    jsonMessage.action = Action.SEND;
+    jsonMessage.message = new Message(recipient);
+    return jsonMessage;
+  }
+}
diff --git a/src/test/java/org/arl/fjage/test/auth/SimpleFirewallSupplier.java b/src/test/java/org/arl/fjage/test/auth/SimpleFirewallSupplier.java
new file mode 100644
index 00000000..099b00c3
--- /dev/null
+++ b/src/test/java/org/arl/fjage/test/auth/SimpleFirewallSupplier.java
@@ -0,0 +1,134 @@
+/******************************************************************************
+
+Copyright (c) 2013, Mandar Chitre
+
+This file is part of fjage which is released under Simplified BSD License.
+See file LICENSE.txt or go to http://www.opensource.org/licenses/BSD-3-Clause
+for full license details.
+
+******************************************************************************/
+
+package org.arl.fjage.test.auth;
+
+import org.arl.fjage.AgentID;
+import org.arl.fjage.auth.Firewall;
+import org.arl.fjage.remote.Action;
+import org.arl.fjage.remote.JsonMessage;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class SimpleFirewallSupplier
+    implements Supplier<Firewall> {
+
+  private final Map<String, UserConfiguration> userConfigurationMap = Collections.synchronizedMap(new HashMap<>());
+
+  public SimpleFirewallSupplier addUserConfiguration(String credentials,
+                                                     Consumer<UserConfiguration> userConfigurationConsumer) {
+    final UserConfiguration userConfiguration = new UserConfiguration();
+    userConfigurationConsumer.accept(userConfiguration);
+    userConfigurationMap.put(credentials, userConfiguration);
+    return this;
+  }
+
+  public UserConfiguration getUserConfiguration(String credentials) {
+    return userConfigurationMap.get(credentials);
+  }
+
+  @Override
+  public Firewall get() {
+    return new SimpleFirewall();
+  }
+
+  private class SimpleFirewall
+      implements Firewall {
+
+    private String creds = null;
+    private UserConfiguration userConfiguration = null;
+
+    @Override
+    public boolean authenticate(String creds) {
+      final UserConfiguration userConfiguration = getUserConfiguration(creds);
+      if (userConfiguration != null) {
+        this.creds = creds;
+        this.userConfiguration = userConfiguration;
+        return true;
+      } else {
+        this.creds = null;
+        this.userConfiguration = null;
+        return false;
+      }
+    }
+
+    @Override
+    public boolean permit(JsonMessage rq) {
+      if (userConfiguration == null) {
+        return false;
+      }
+      return userConfiguration.isPermitted(rq);
+    }
+
+    @Override
+    public boolean permit(AgentID aid) {
+      if (userConfiguration == null) {
+        return false;
+      }
+      return userConfiguration.isPermitted(aid);
+    }
+
+    @Override
+    public void signoff() {
+      // do nothing
+    }
+
+    @Override
+    public String toString() {
+      if (creds != null)  {
+        return String.format("@%x/%s", hashCode(), creds);
+      } else {
+        return String.format("@%x", hashCode());
+      }
+    }
+  }
+
+  public static class UserConfiguration {
+
+    private final Set<String> allowedServiceNames = new HashSet<>();
+    private final Set<String> allowedAgentNames = new HashSet<>();
+    private final Set<String> allowedTopicNames = new HashSet<>();
+
+    public UserConfiguration allowedServiceNames(String... serviceNames) {
+      allowedServiceNames.addAll(Arrays.asList(serviceNames));
+      return this;
+    }
+
+    public UserConfiguration allowedAgentNames(String... agentNames) {
+      allowedAgentNames.addAll(Arrays.asList(agentNames));
+      return this;
+    }
+
+    public UserConfiguration allowedTopicNames(String... topicNames) {
+      allowedTopicNames.addAll(Arrays.asList(topicNames));
+      return this;
+    }
+
+    public boolean isPermitted(AgentID agentID) {
+      if (agentID.isTopic()) {
+        return allowedTopicNames.contains(agentID.getName());
+      } else {
+        return true;
+      }
+    }
+
+    public boolean isPermitted(JsonMessage jsonMessage) {
+      if ((jsonMessage.action == Action.AGENT_FOR_SERVICE) || (jsonMessage.action == Action.AGENTS_FOR_SERVICE)) {
+        return allowedServiceNames.contains(jsonMessage.service);
+      } else if (jsonMessage.action == Action.SEND) {
+        return allowedAgentNames.contains(jsonMessage.message.getRecipient().getName());
+      } else {
+        return true;
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/arl/fjage/test/auth/TracedFirewallSupplier.java b/src/test/java/org/arl/fjage/test/auth/TracedFirewallSupplier.java
new file mode 100644
index 00000000..a892bb6a
--- /dev/null
+++ b/src/test/java/org/arl/fjage/test/auth/TracedFirewallSupplier.java
@@ -0,0 +1,75 @@
+/******************************************************************************
+
+Copyright (c) 2013, Mandar Chitre
+
+This file is part of fjage which is released under Simplified BSD License.
+See file LICENSE.txt or go to http://www.opensource.org/licenses/BSD-3-Clause
+for full license details.
+
+******************************************************************************/
+
+package org.arl.fjage.test.auth;
+
+import org.arl.fjage.AgentID;
+import org.arl.fjage.auth.Firewall;
+import org.arl.fjage.remote.JsonMessage;
+
+import java.util.function.Supplier;
+import java.util.logging.Logger;
+
+public class TracedFirewallSupplier
+    implements Supplier<Firewall> {
+
+  private final Supplier<Firewall> fwSupplier;
+  private final Logger log = Logger.getLogger(getClass().getName());
+
+  public TracedFirewallSupplier(Supplier<Firewall> fwSupplier) {
+    super();
+
+    this.fwSupplier = fwSupplier;
+  }
+
+  @Override
+  public Firewall get() {
+    return new TracedFirewall(fwSupplier.get());
+  }
+
+  private class TracedFirewall
+      implements Firewall {
+
+    private final Firewall fw;
+
+    public TracedFirewall(Firewall fw) {
+      super();
+
+      this.fw = fw;
+    }
+
+    @Override
+    public boolean authenticate(String creds) {
+      final boolean auth = fw.authenticate(creds);
+      log.info(String.format("[%s] authenticate(%s)=%s", fw, creds, auth));
+      return auth;
+    }
+
+    @Override
+    public boolean permit(JsonMessage rq) {
+      final boolean permitted = fw.permit(rq);
+      log.info(String.format("[%s] permit(%s)=%s", fw, rq.toJson(), permitted));
+      return permitted;
+    }
+
+    @Override
+    public boolean permit(AgentID aid) {
+      final boolean permitted = fw.permit(aid);
+      log.info(String.format("[%s] permit(%s)=%s", fw, aid, permitted));
+      return permitted;
+    }
+
+    @Override
+    public void signoff() {
+      fw.signoff();
+      log.info(String.format("[%s] signoff()", fw));
+    }
+  }
+}