OS-1 : insecure UI websocket.
- notes on authentication of UI web socket connection.
- new classes: UiSessionToken, UiTokenService.
- UiExtensionManager now implements UiTokenService.
- UiWebSocket now expects an authentication event from the client
- websocket.js now sends authentication event as first event
- (fix websocket Jasmine test)

Change-Id: I4303c67f57fc618e911be244091f00bcc2823c91
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
index 8713e9e..854c010 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
@@ -22,6 +22,8 @@
 import org.onosproject.rest.AbstractInjectionResource;
 import org.onosproject.ui.UiExtensionService;
 import org.onosproject.ui.UiPreferencesService;
+import org.onosproject.ui.UiSessionToken;
+import org.onosproject.ui.UiTokenService;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -67,8 +69,12 @@
     public Response getMainIndex() throws IOException {
         ClassLoader classLoader = getClass().getClassLoader();
         UiExtensionService service;
+        UiTokenService tokens;
+
         try {
             service = get(UiExtensionService.class);
+            tokens = get(UiTokenService.class);
+
         } catch (ServiceNotFoundException e) {
             return Response.ok(classLoader.getResourceAsStream(NOT_READY)).build();
         }
@@ -84,9 +90,17 @@
         int p2e = split(index, p2s, INJECT_CSS_END);
         int p3s = split(index, p2e, null);
 
+
         // FIXME: use global opaque auth token to allow secure failover
+
+        // for now, just use the user principal name...
         String userName = ctx.getUserPrincipal().getName();
-        String auth = "var onosAuth='" + userName + "';\n";
+
+        // get a session token to use for UI-web-socket authentication
+        UiSessionToken token = tokens.issueToken(userName);
+
+        String auth = "var onosUser='" + userName + "',\n" +
+                      "    onosAuth='" + token + "';\n";
 
         StreamEnumeration streams =
                 new StreamEnumeration(of(stream(index, 0, p0s),
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index 5e36357..9a016cf 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -50,6 +50,8 @@
 import org.onosproject.ui.UiExtensionService;
 import org.onosproject.ui.UiMessageHandlerFactory;
 import org.onosproject.ui.UiPreferencesService;
+import org.onosproject.ui.UiSessionToken;
+import org.onosproject.ui.UiTokenService;
 import org.onosproject.ui.UiTopo2OverlayFactory;
 import org.onosproject.ui.UiTopoMap;
 import org.onosproject.ui.UiTopoMapFactory;
@@ -62,6 +64,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.math.BigInteger;
+import java.security.SecureRandom;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -83,11 +87,13 @@
 @Component(immediate = true)
 @Service
 public class UiExtensionManager
-        implements UiExtensionService, UiPreferencesService, SpriteService {
+        implements UiExtensionService, UiPreferencesService, SpriteService,
+        UiTokenService {
 
     private static final ClassLoader CL = UiExtensionManager.class.getClassLoader();
 
     private static final String ONOS_USER_PREFERENCES = "onos-ui-user-preferences";
+    private static final String ONOS_SESSION_TOKENS = "onos-ui-session-tokens";
     private static final String CORE = "core";
     private static final String GUI_ADDED = "guiAdded";
     private static final String GUI_REMOVED = "guiRemoved";
@@ -120,6 +126,12 @@
     private final MapEventListener<String, ObjectNode> prefsListener =
             new InternalPrefsListener();
 
+    // Session tokens
+    private ConsistentMap<UiSessionToken, String> tokensConsistentMap;
+    private Map<UiSessionToken, String> tokens;
+    private final SessionTokenGenerator tokenGen =
+            new SessionTokenGenerator();
+
     private final ObjectMapper mapper = new ObjectMapper();
 
     private final ExecutorService eventHandlingExecutor =
@@ -216,7 +228,7 @@
                      JsonNodeFactory.class, LinkedHashMap.class,
                      TextNode.class, BooleanNode.class,
                      LongNode.class, DoubleNode.class, ShortNode.class,
-                     IntNode.class, NullNode.class);
+                     IntNode.class, NullNode.class, UiSessionToken.class);
 
         prefsConsistentMap = storageService.<String, ObjectNode>consistentMapBuilder()
                 .withName(ONOS_USER_PREFERENCES)
@@ -225,6 +237,14 @@
                 .build();
         prefsConsistentMap.addListener(prefsListener);
         prefs = prefsConsistentMap.asJavaMap();
+
+        tokensConsistentMap = storageService.<UiSessionToken, String>consistentMapBuilder()
+                .withName(ONOS_SESSION_TOKENS)
+                .withSerializer(serializer)
+                .withRelaxedReadConsistency()
+                .build();
+        tokens = tokensConsistentMap.asJavaMap();
+
         register(core);
         log.info("Started");
     }
@@ -325,6 +345,55 @@
         return key.split(SLASH)[IDX_KEY];
     }
 
+
+    // =====================================================================
+    // UiTokenService
+
+    @Override
+    public UiSessionToken issueToken(String username) {
+        UiSessionToken token = new UiSessionToken(tokenGen.nextSessionId());
+        tokens.put(token, username);
+        log.debug("UiSessionToken issued: {}", token);
+        return token;
+    }
+
+    @Override
+    public void revokeToken(UiSessionToken token) {
+        if (token != null) {
+            tokens.remove(token);
+            log.debug("UiSessionToken revoked: {}", token);
+        }
+    }
+
+    @Override
+    public boolean isTokenValid(UiSessionToken token) {
+        return token != null && tokens.containsKey(token);
+    }
+
+    private final class SessionTokenGenerator {
+        private final SecureRandom random = new SecureRandom();
+
+        /*
+            This works by choosing 130 bits from a cryptographically secure
+            random bit generator, and encoding them in base-32.
+
+            128 bits is considered to be cryptographically strong, but each
+            digit in a base 32 number can encode 5 bits, so 128 is rounded up
+            to the next multiple of 5.
+
+            This encoding is compact and efficient, with 5 random bits per
+            character. Compare this to a random UUID, which only has 3.4 bits
+            per character in standard layout, and only 122 random bits in total.
+
+            Note that SecureRandom objects are expensive to initialize, so
+            we'll want to keep it around and re-use it.
+         */
+
+        private String nextSessionId() {
+            return new BigInteger(130, random).toString(32);
+        }
+    }
+
     // Auxiliary listener to preference map events.
     private class InternalPrefsListener
             implements MapEventListener<String, ObjectNode> {
@@ -340,7 +409,7 @@
 
         private ObjectNode jsonPrefs() {
             ObjectNode json = mapper.createObjectNode();
-            prefs.entrySet().forEach(e -> json.set(keyName(e.getKey()), e.getValue()));
+            prefs.forEach((key, value) -> json.set(keyName(key), value));
             return json;
         }
     }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
index 8bb6244..6c4fc1e 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
@@ -29,6 +29,8 @@
 import org.onosproject.ui.UiExtensionService;
 import org.onosproject.ui.UiMessageHandler;
 import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiSessionToken;
+import org.onosproject.ui.UiTokenService;
 import org.onosproject.ui.UiTopo2OverlayFactory;
 import org.onosproject.ui.UiTopoLayoutService;
 import org.onosproject.ui.UiTopoOverlayFactory;
@@ -57,6 +59,9 @@
     private static final String EVENT = "event";
     private static final String PAYLOAD = "payload";
     private static final String UNKNOWN = "unknown";
+    private static final String AUTHENTICATION = "authentication";
+    private static final String TOKEN = "token";
+    private static final String ERROR = "error";
 
     private static final String ID = "id";
     private static final String IP = "ip";
@@ -87,6 +92,9 @@
     private TopoOverlayCache overlayCache;
     private Topo2OverlayCache overlay2Cache;
 
+    private UiSessionToken sessionToken;
+
+
     /**
      * Creates a new web-socket for serving data to the Web UI.
      *
@@ -102,8 +110,8 @@
         UiTopoLayoutService layoutService = directory.get(UiTopoLayoutService.class);
 
         sharedModel.injectJsonifier(t2json);
-
         topoSession = new UiTopoSession(this, t2json, sharedModel, layoutService);
+        sessionToken = null;
     }
 
     @Override
@@ -192,6 +200,11 @@
 
     @Override
     public synchronized void onClose(int closeCode, String message) {
+        tokenService().revokeToken(sessionToken);
+        log.info("Session token revoked");
+
+        sessionToken = null;
+
         topoSession.destroy();
         destroyHandlersAndOverlays();
         log.info("GUI client disconnected [close-code={}, message={}]",
@@ -210,13 +223,20 @@
         try {
             ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
             String type = message.path(EVENT).asText(UNKNOWN);
-            UiMessageHandler handler = handlers.get(type);
-            if (handler != null) {
-                log.debug("RX message: {}", message);
-                handler.process(message);
+
+            if (sessionToken == null) {
+                authenticate(type, message);
+
             } else {
-                log.warn("No GUI message handler for type {}", type);
+                UiMessageHandler handler = handlers.get(type);
+                if (handler != null) {
+                    log.debug("RX message: {}", message);
+                    handler.process(message);
+                } else {
+                    log.warn("No GUI message handler for type {}", type);
+                }
             }
+
         } catch (Exception e) {
             log.warn("Unable to parse GUI message {} due to {}", data, e);
             log.debug("Boom!!!", e);
@@ -277,6 +297,31 @@
         log.debug("#handlers = {}, #overlays = {}", handlers.size(), overlayCache.size());
     }
 
+    private void authenticate(String type, ObjectNode message) {
+        if (!AUTHENTICATION.equals(type)) {
+            log.warn("Non-Authenticated Web Socket: {}", message);
+            return;
+        }
+
+        String tokstr = message.path(PAYLOAD).path(TOKEN).asText(UNKNOWN);
+        UiSessionToken token = new UiSessionToken(tokstr);
+
+        if (tokenService().isTokenValid(token)) {
+            sessionToken = token;
+            log.info("Session token authenticated");
+            log.debug("WebSocket authenticated: {}", message);
+        } else {
+            log.warn("Invalid Authentication Token: {}", message);
+            sendMessage(ERROR, notAuthorized(token));
+        }
+    }
+
+    private ObjectNode notAuthorized(UiSessionToken token) {
+        return mapper.createObjectNode()
+                .put("message", "invalid authentication token")
+                .put("badToken", token.toString());
+    }
+
     private void registerOverlays(UiExtension ext) {
         UiTopoOverlayFactory overlayFactory = ext.topoOverlayFactory();
         if (overlayFactory != null) {
@@ -350,4 +395,11 @@
         sendMessage(BOOTSTRAP, payload);
     }
 
+    private UiTokenService tokenService() {
+        UiTokenService service = directory.get(UiTokenService.class);
+        if (service == null) {
+            log.error("Unable to reference UiTokenService");
+        }
+        return service;
+    }
 }