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/core/api/src/main/java/org/onosproject/ui/UiSessionToken.java b/core/api/src/main/java/org/onosproject/ui/UiSessionToken.java
new file mode 100644
index 0000000..b4b3d49
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/UiSessionToken.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.onosproject.ui;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Encapsulates a session token pertaining to a Web UI websocket connection.
+ */
+public class UiSessionToken {
+
+    private final String token;
+
+    /**
+     * Creates a session token from the given string.
+     *
+     * @param token the token in string form
+     * @throws NullPointerException if token is null
+     */
+    public UiSessionToken(String token) {
+        this.token = checkNotNull(token);
+    }
+
+    @Override
+    public String toString() {
+        return token;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        UiSessionToken that = (UiSessionToken) o;
+        return token.equals(that.token);
+    }
+
+    @Override
+    public int hashCode() {
+        return token.hashCode();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/UiTokenService.java b/core/api/src/main/java/org/onosproject/ui/UiTokenService.java
new file mode 100644
index 0000000..37e209d
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/UiTokenService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.onosproject.ui;
+
+/**
+ * Service for handling UI session tokens.
+ */
+public interface UiTokenService {
+
+    /**
+     * Issues a session token. The service will generate a new token,
+     * publish it in the distributed map of valid UI session tokens, and
+     * return it to the caller.
+     *
+     * @param username the username to be associated with the token.
+     * @return the token
+     */
+    UiSessionToken issueToken(String username);
+
+    /**
+     * Revokes the specified session token. The service will remove the token
+     * from the distributed map of valid UI session tokens.
+     *
+     * @param token the token to be revoked
+     */
+    void revokeToken(UiSessionToken token);
+
+    /**
+     * Returns true if the specified token is currently in the distributed
+     * map of valid UI session tokens.
+     *
+     * @param token the token to check
+     * @return true, if the token is currently valid; false otherwise
+     */
+    boolean isTokenValid(UiSessionToken token);
+}
diff --git a/web/gui/doc/notes-websocket.md b/web/gui/doc/notes-websocket.md
new file mode 100644
index 0000000..6b90d73
--- /dev/null
+++ b/web/gui/doc/notes-websocket.md
@@ -0,0 +1,76 @@
+# UI Web Socket Session Establishment
+
+(1) Web client accesses index.html but is redirected to login page for
+    basic authentication.
+
+(2) `MainIndexResource` (protected page, user is now authenticated) requests
+    a token to be generated by the `UiTokenService`.
+
+(3) `UiTokenService` generates token, adds it to distributed map as
+    entry `{token -> username}`, and returns token to `MainIndexResource`.
+
+(4) `MainIndexResource` embeds username and token in `index.html`.
+
+(5) Web client opens web socket connection (promoted from http). Note that
+    the `UiWebSocket` instance is not marked as "authenticated" yet...
+
+
+(6) `UiWebSocket` sends bootstrap data (list of ONOS cluster node IPs)
+
+(7) Web client sends initial message "uiAuthenticate", along with username
+    and authentication token (picked up from `index.html`).
+
+(8) `UiWebsocket` verifies that token is valid via the `UiTokenService`, and
+    marks itself as "authenticated".
+
+(9) Subsequent `onMessage()` calls to `UiWebSocket` only proceed if 
+    "authenticated" is true.
+
+(10) User logs out of ONOS UI, generates onClose() call.
+
+(11) `UiWebSocket` requests the token be revoked.
+
+(12) `UiTokenService` unmaps the token from the distributed map.
+
+
+```
+ WebClient           MainIndex           UiToken           WebSocket
+ ----+----           ----+----           ---+---           ----+----
+     |            login* |                  |                  |    * basic
+(1)  o------------------>|                  |                  |     auth'n 
+     |                   |  issueToken(usr) |                  |
+(2)  |                   o----------------->|                  |
+     |                   |                  o- map token in    |
+(3)  |                   | tkn              |  distrib. map    |
+     | index.html(tkn)   |<-----------------o                  |
+(4)  |<------------------o                  |                  |
+     |                   |                  |           onOpen |
+(5)  o-------------------------------------------------------->|
+     | bootstrapData     |                  |                  |
+(6)  |<--------------------------------------------------------o
+     |                   |                  |                  |
+     |                   |                  |   onMsg(usr,tkn) |
+(7)  o-------------------------------------------------------->|
+     |                   |                  | isValid(tkn)     |
+(8)  |                   |                  |<-----------------o
+     |                   |                  o----------------->| 
+     |                   |                  |                  o- mark socket
+     |                   |                  |                  |  valid
+     |                   |                  |                  |
+     |                   |                  |       onMsg(...) |
+(9)  o-------------------------------------------------------->|
+     |                   |                  |                  o- only processed
+     |                   |                  |                  |  if socket valid
+     
+     :                   :                  :                  :
+     
+     |                   |                  |          onClose |
+(10) o-------------------------------------------------------->|
+     |                   |                  | revoke(tkn)      |
+(11) |                   |                  |<-----------------o
+(12) |                   |                  o- unmap token in  |
+     |                   |                  |  distrib. map    |
+     |                   |                  |                  |
+```
+
+
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;
+    }
 }
diff --git a/web/gui/src/main/webapp/app/fw/mast/mast.js b/web/gui/src/main/webapp/app/fw/mast/mast.js
index 4d8b5fa..6483703 100644
--- a/web/gui/src/main/webapp/app/fw/mast/mast.js
+++ b/web/gui/src/main/webapp/app/fw/mast/mast.js
@@ -84,8 +84,8 @@
                 ns.toggleNav();
             };
 
-            // onosAuth is a global set via the index.html generated source
-            $scope.user = onosAuth || '(no one)';
+            // onosUser is a global set via the index.html generated source
+            $scope.user = onosUser || '(no one)';
             $scope.helpTip = 'Show help page for current view';
 
             $scope.directTo = function () {
diff --git a/web/gui/src/main/webapp/app/fw/remote/websocket.js b/web/gui/src/main/webapp/app/fw/remote/websocket.js
index 55c7b40..5922c60 100644
--- a/web/gui/src/main/webapp/app/fw/remote/websocket.js
+++ b/web/gui/src/main/webapp/app/fw/remote/websocket.js
@@ -38,10 +38,9 @@
         nextListenerId = 1,     // internal ID for open listeners
         loggedInUser = null;    // name of logged-in user
 
-    // =======================
-    // === Bootstrap Handler
-
+    // built-in handlers
     var builtinHandlers = {
+
         bootstrap: function (data) {
             $log.debug('bootstrap data', data);
             loggedInUser = data.user;
@@ -53,6 +52,18 @@
                     // TODO: add connect info to masthead somewhere
                 }
             });
+        },
+
+        error: function (data) {
+            var m = data.message || 'error from server';
+            $log.error(m, data);
+
+            // Unrecoverable error - throw up the veil...
+            vs && vs.show([
+                'Oops!',
+                'Server reports error...',
+                m
+            ]);
         }
     };
 
@@ -183,7 +194,7 @@
 
     // Currently supported opts:
     //   wsport: web socket port (other than default 8181)
-    // host: if defined, is the host address to use
+    //   host:   if defined, is the host address to use
     function createWebSocket(opts, _host_) {
         var wsport = (opts && opts.wsport) || null;
 
@@ -198,6 +209,8 @@
             ws.onopen = handleOpen;
             ws.onmessage = handleMessage;
             ws.onclose = handleClose;
+
+            sendEvent('authentication', {token: onosAuth});
         }
         // Note: Wsock logs an error if the new WebSocket call fails
         return url;
diff --git a/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js b/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js
index fc7aa09..69898ef 100644
--- a/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js
@@ -121,7 +121,10 @@
             payload: { mock: 'thing' }
         };
         wss.sendEvent(fakeEvent.event, fakeEvent.payload);
-        expect(mockWebSocket.send).not.toHaveBeenCalled();
+        // on opening the socket, a single authentication event should have
+        // been sent already...
+        expect(mockWebSocket.send.calls.count()).toEqual(1);
+
         wss.createWebSocket({ wsport: 1234 });
         mockWebSocket.onopen();
         expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(fakeEvent));
diff --git a/web/gui/src/main/webapp/tests/server.mock.js b/web/gui/src/main/webapp/tests/server.mock.js
index 3ecdbdb..3d10b11 100644
--- a/web/gui/src/main/webapp/tests/server.mock.js
+++ b/web/gui/src/main/webapp/tests/server.mock.js
@@ -1,2 +1,3 @@
-onosAuth = '';
+onosUser = 'onos';
+onosAuth = 'foo';
 userPrefs = {};
\ No newline at end of file