metron-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From rmerri...@apache.org
Subject [metron] branch master updated: METRON-1878 Add Metron as a Knox service (merrimanr) closes apache/metron#1275
Date Thu, 03 Jan 2019 18:14:04 GMT
This is an automated email from the ASF dual-hosted git repository.

rmerriman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/metron.git


The following commit(s) were added to refs/heads/master by this push:
     new ef69acd  METRON-1878 Add Metron as a Knox service (merrimanr) closes apache/metron#1275
ef69acd is described below

commit ef69acd0d911ddde2dec17544eb1a844b7b79bee
Author: merrimanr <merrimanr@gmail.com>
AuthorDate: Thu Jan 3 12:13:48 2019 -0600

    METRON-1878 Add Metron as a Knox service (merrimanr) closes apache/metron#1275
---
 dependencies_with_url.csv                          |   3 +
 .../packaging/docker/rpm-docker/SPECS/metron.spec  |  10 +
 metron-interface/README.md                         |  75 ++++
 metron-interface/flow_diagram.png                  | Bin 0 -> 38433 bytes
 metron-interface/flow_diagrams.pptx                | Bin 0 -> 42282 bytes
 metron-interface/knox_flow_diagram.png             | Bin 0 -> 89684 bytes
 .../cypress/integration/pcap/pcap.spec.js          |   6 +-
 metron-interface/metron-alerts/proxy.conf.json     |   4 -
 .../metron-alerts/src/app/app-routing.module.ts    |   2 +-
 .../metron-alerts/src/app/app.component.html       |   2 +-
 .../src/app/pcap/service/pcap.service.spec.ts      |   4 +
 .../src/app/service/app-config.service.ts          |  14 +-
 .../src/app/service/authentication.service.ts      |  21 +-
 .../metron-alerts/src/app/shared/auth-guard.ts     |   6 +-
 .../shared/directives/alert-search.directive.ts    |   2 +-
 .../metron-alerts/src/app/shared/login-guard.ts    |   2 +-
 .../metron-alerts/src/app/utils/httpUtil.ts        |   8 +-
 .../metron-alerts/src/assets/app-config.json       |   3 +-
 metron-interface/metron-alerts/src/index.html      |   3 +-
 metron-interface/metron-config/src/app/_fonts.scss |  24 +-
 .../metron-config/src/app/app.component.spec.ts    |  15 +-
 .../metron-config/src/app/app.module.ts            |  14 +-
 .../metron-config/src/app/app.routes.ts            |   2 +-
 .../general-settings.component.spec.ts             |  13 +-
 .../src/app/navbar/navbar.component.ts             |   3 +-
 ...sensor-parser-config-readonly.component.spec.ts |  53 +--
 .../sensor-parser-config.component.spec.ts         |  55 +--
 .../sensor-parser-list.component.spec.ts           |  35 +-
 .../src/app/service/app-config.service.ts          |  14 +-
 .../src/app/service/authentication.service.spec.ts |  18 +-
 .../src/app/service/authentication.service.ts      |  29 +-
 .../src/app/service/global-config.service.spec.ts  |   4 +-
 .../src/app/service/global-config.service.ts       |   7 +-
 .../app/service/grok-validation.service.spec.ts    |   5 +-
 .../src/app/service/grok-validation.service.ts     |   7 +-
 .../src/app/service/hdfs.service.spec.ts           |   4 +-
 .../metron-config/src/app/service/hdfs.service.ts  |   7 +-
 .../src/app/service/kafka.service.spec.ts          |   5 +-
 .../metron-config/src/app/service/kafka.service.ts |   7 +-
 .../src/app/service/mock.app-config.service.ts}    |  24 +-
 .../sensor-enrichment-config.service.spec.ts       |   4 +-
 .../service/sensor-enrichment-config.service.ts    |   7 +-
 .../service/sensor-indexing-config.service.spec.ts |   5 +-
 .../app/service/sensor-indexing-config.service.ts  |   7 +-
 .../sensor-parser-config-history.service.spec.ts   |   5 +-
 .../sensor-parser-config-history.service.ts        |   7 +-
 .../service/sensor-parser-config.service.spec.ts   |   4 +-
 .../app/service/sensor-parser-config.service.ts    |   7 +-
 .../src/app/service/stellar.service.spec.ts        |   5 +-
 .../src/app/service/stellar.service.ts             |   7 +-
 .../src/app/service/storm.service.spec.ts          |   5 +-
 .../metron-config/src/app/service/storm.service.ts |   7 +-
 .../app/shared/ace-editor/ace-editor.component.ts  |   2 +-
 .../src/app/shared/auth-guard.spec.ts              |  20 +-
 .../metron-config/src/app/shared/auth-guard.ts     |   3 +-
 .../src/app/shared/login-guard.spec.ts             |  13 +-
 .../metron-config/src/app/shared/login-guard.ts    |   2 +-
 .../metron-config/src/app/util/httpUtil.ts         |   8 +-
 .../metron-config/src/assets/app-config.json       |   4 +
 metron-interface/metron-config/src/index.html      |   3 +-
 metron-interface/metron-rest/pom.xml               |   6 +
 .../main/config/knox/conf/topologies/metron.xml    |  56 +++
 .../main/config/knox/conf/topologies/metronsso.xml | 100 ++++++
 .../config/knox/data/services/alerts/rewrite.xml   |  24 ++
 .../config/knox/data/services/alerts/service.xml}  |  29 +-
 .../knox/data/services/management/rewrite.xml      |  24 ++
 .../knox/data/services/management/service.xml      |  21 ++
 .../config/knox/data/services/rest/rewrite.xml}    |  26 +-
 .../config/knox/data/services/rest/service.xml}    |  27 +-
 .../apache/metron/rest/MetronRestConstants.java    |   8 +
 .../rest/config/KnoxSSOAuthenticationFilter.java   | 278 +++++++++++++++
 .../org/apache/metron/rest/config/LdapConfig.java  |  52 +++
 .../apache/metron/rest/config/SwaggerConfig.java   |  46 ++-
 .../metron/rest/config/WebSecurityConfig.java      |  57 ++-
 .../metron/rest/controller/AlertsUIController.java |   5 +-
 .../apache/metron/rest/security/SecurityUtils.java |  33 ++
 .../src/main/scripts/install_metron_knox.sh        |  34 ++
 .../config/KnoxSSOAuthenticationFilterTest.java    | 388 +++++++++++++++++++++
 78 files changed, 1474 insertions(+), 375 deletions(-)

diff --git a/dependencies_with_url.csv b/dependencies_with_url.csv
index 745e3c9..5856a3a 100644
--- a/dependencies_with_url.csv
+++ b/dependencies_with_url.csv
@@ -495,3 +495,6 @@ org.fusesource.jansi:jansi:jar:1.16:compile,ASLv2,https://github.com/fusesource/
 com.vividsolutions:jts:jar:1.13:compile
 joda-time:joda-time:jar:2.10:compile
 org.elasticsearch:securesm:jar:1.2:compile
+com.github.stephenc.jcip:jcip-annotations:jar:1.0-1:compile,ASLv2,http://stephenc.github.io/jcip-annotations/
+com.nimbusds:nimbus-jose-jwt:jar:4.41.2:compile,ASLv2,https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home
+
diff --git a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
index 294e24b..8f0cd9d 100644
--- a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
+++ b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec
@@ -467,8 +467,17 @@ This package installs the Metron Rest %{metron_home}
 %dir %{metron_home}/bin
 %dir %{metron_home}/lib
 %{metron_home}/config/rest_application.yml
+%{metron_home}/config/knox/conf/topologies/metron.xml
+%{metron_home}/config/knox/conf/topologies/metronsso.xml
+%{metron_home}/config/knox/data/services/alerts/rewrite.xml
+%{metron_home}/config/knox/data/services/alerts/service.xml
+%{metron_home}/config/knox/data/services/management/rewrite.xml
+%{metron_home}/config/knox/data/services/management/service.xml
+%{metron_home}/config/knox/data/services/rest/rewrite.xml
+%{metron_home}/config/knox/data/services/rest/service.xml
 %{metron_home}/bin/metron-rest.sh
 %{metron_home}/bin/pcap_to_pdml.sh
+%{metron_home}/bin/install_metron_knox.sh
 %attr(0644,root,root) %{metron_home}/lib/metron-rest-%{full_version}.jar
 
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -520,6 +529,7 @@ This package installs the Metron Management UI %{metron_home}
 %attr(0644,root,root) %{metron_home}/web/management-ui/assets/fonts/Roboto/LICENSE.txt
 %attr(0644,root,root) %{metron_home}/web/management-ui/assets/fonts/Roboto/*.ttf
 %attr(0644,root,root) %{metron_home}/web/management-ui/assets/images/*
+%attr(0644,root,root) %{metron_home}/web/management-ui/assets/app-config.json
 %attr(0644,root,root) %{metron_home}/web/management-ui/license/*
 
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/metron-interface/README.md b/metron-interface/README.md
new file mode 100644
index 0000000..639667f
--- /dev/null
+++ b/metron-interface/README.md
@@ -0,0 +1,75 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+# Metron Interface
+
+Metron interface contains code and assets to support the various web applications in Metron.  The existing modules are:
+
+* metron-alerts : An Angular application that exposes a way to browse, filter and act on alerts.
+* metron-config: An Angular application that allows an administrator to configure and maintain Metron.
+* metron-rest: A Spring REST application that supports the UI and exposes Metron features through a REST/json interface.
+* metron-rest-client: Model objects used for passing data back and forth in the REST application.  A Metron client would use these classes for de/serializing requests and responses.
+
+## Architecture
+
+The UIs and REST server are all run in separate web containers.  The UIs are served from separate [Express](https://expressjs.com/) servers that are configured to proxy REST requests
+to the REST application.  Proxying REST requests satisfies the same-origin browser restriction because all requests go to Express, or the same origin.  
+
+REST requests are handled by a [Spring Boot](https://spring.io/projects/spring-boot) application.  A [Swagger](https://swagger.io/) interface is available and served by the REST application.
+
+### Security
+
+The UIs depend on REST for authentication.  Credentials are passed with [Basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) and only REST requests require
+authentication, static assets are not secured.  Once authentication has been performed a session is established and a cookie is stored and used to authenticate subsequent requests.
+
+### Request/Response Flow
+
+The following diagram illustrates the flow of data for the various types of requests:
+
+![Flow Diagram](flow_diagram.png)
+
+
+## Architecture with Knox
+
+[Apache Knox](https://knox.apache.org/) is a "REST API and Application Gateway for the Apache Hadoop Ecosystem".  It can be enabled for Metron and provides several security benefits:
+
+* All requests go through Knox so same-origin browser restrictions are not a concern.
+* Knox, in combination with a firewall, can restrict traffic to always go through Knox.  This greatly reduces the security attack surface area of the UIs and REST application.
+* Provides access to other common Apache Hadoop services
+* Provides a single sign on experience between the UIs and REST application
+* All requests can be protected and secured
+
+We primarily use Knox's proxying and authentication services.  Knox acts as a reverse proxy for all UIs and the REST application.  
+
+### Knox Security
+
+With Knox enabled, Knox now handles authentication when accessing the UIs and REST together.  Basic authentication is still an option for making requests directly to the REST application.  Any request to the UIs must go through Knox first and contain the proper security token.  
+If a valid token is not found, Knox will redirect to the Knox SSO login form.  Once a valid token is found, Knox will then redirect to the original url and the request will be forwarded on.  Accessing the REST application through Knox also follows this pattern.
+The UIs make REST requests this way with Knox enabled since they no longer depend on Express to proxy requests.  The context path now determines which type of request it is rather than the host and port.  
+
+REST still requires authentication so a filter is provided that can validate a Knox token using token properties and a Knox public key.  The REST application also supports Basic authentication.  Since both Knox and the REST application should use
+the same authentication mechanism, LDAP authentication is required for the REST application.
+
+Roles are mapped directly to LDAP groups when Knox is enabled for REST.  LDAP group names are converted to upper case and prepended with "ROLE_".  For example, if a user's groups in LDAP were "user" and "admin", the corresponding roles in REST with Knox enabled would be "ROLE_USER" and "ROLE_ADMIN".
+
+### Knox Request/Response Flow
+
+The following diagram illustrates the flow of data for the various types of requests when Knox is enabled:
+
+![Knox Flow Diagram](knox_flow_diagram.png)
+
+Note how the flow diagrams for Static asset requests and Rest requests (through Knox) are identical.
diff --git a/metron-interface/flow_diagram.png b/metron-interface/flow_diagram.png
new file mode 100644
index 0000000..835db79
Binary files /dev/null and b/metron-interface/flow_diagram.png differ
diff --git a/metron-interface/flow_diagrams.pptx b/metron-interface/flow_diagrams.pptx
new file mode 100644
index 0000000..fcaa7a6
Binary files /dev/null and b/metron-interface/flow_diagrams.pptx differ
diff --git a/metron-interface/knox_flow_diagram.png b/metron-interface/knox_flow_diagram.png
new file mode 100644
index 0000000..7a41867
Binary files /dev/null and b/metron-interface/knox_flow_diagram.png differ
diff --git a/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js b/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js
index 835c714..fa0cd51 100644
--- a/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js
+++ b/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js
@@ -26,9 +26,9 @@ context('PCAP Tab', () => {
       response: 'user'
     });
     cy.route({
-      method: 'POST',
-      url: 'logout',
-      response: []
+        method: 'POST',
+        url: '/api/v1/logout',
+        response: []
     });
 
     cy.route('GET', '/api/v1/global/config', 'fixture:config.json');
diff --git a/metron-interface/metron-alerts/proxy.conf.json b/metron-interface/metron-alerts/proxy.conf.json
index 612bd67..571af9a 100644
--- a/metron-interface/metron-alerts/proxy.conf.json
+++ b/metron-interface/metron-alerts/proxy.conf.json
@@ -2,9 +2,5 @@
   "/api/v1": {
     "target": "http://node1:8082",
     "secure": false
-  },
-  "/logout": {
-    "target": "http://node1:8082",
-    "secure": false
   }
 }
diff --git a/metron-interface/metron-alerts/src/app/app-routing.module.ts b/metron-interface/metron-alerts/src/app/app-routing.module.ts
index db6e29f..f899a15 100644
--- a/metron-interface/metron-alerts/src/app/app-routing.module.ts
+++ b/metron-interface/metron-alerts/src/app/app-routing.module.ts
@@ -21,7 +21,7 @@ import {AuthGuard} from './shared/auth-guard';
 import {LoginGuard} from './shared/login-guard';
 
 const routes: Routes = [
-  { path: '',  redirectTo: 'login', pathMatch: 'full'},
+  { path: '',  redirectTo: 'alerts-list', pathMatch: 'full'},
   { path: 'login', loadChildren: 'app/login/login.module#LoginModule', canActivate: [LoginGuard]},
   { path: 'alerts-list', loadChildren: 'app/alerts/alerts-list/alerts-list.module#AlertsListModule', canActivate: [AuthGuard]},
   { path: 'save-search', loadChildren: 'app/alerts/save-search/save-search.module#SaveSearchModule', canActivate: [AuthGuard]},
diff --git a/metron-interface/metron-alerts/src/app/app.component.html b/metron-interface/metron-alerts/src/app/app.component.html
index d94a6d7..1e1e9bf 100644
--- a/metron-interface/metron-alerts/src/app/app.component.html
+++ b/metron-interface/metron-alerts/src/app/app.component.html
@@ -14,7 +14,7 @@
 <div class="container-fluid px-0" [class.notransition]="noTransition">
     <nav class="navbar" *ngIf="loggedIn">
         <a class="" href="#">
-            <img alt="" src="../assets/images/logo.png" width="135" height="45">
+            <img alt="" src="assets/images/logo.png" width="135" height="45">
         </a>
         <ul class="nav ml-4">
             <li class="nav-item" routerLinkActive="active">
diff --git a/metron-interface/metron-alerts/src/app/pcap/service/pcap.service.spec.ts b/metron-interface/metron-alerts/src/app/pcap/service/pcap.service.spec.ts
index 98537c2..8a21363 100644
--- a/metron-interface/metron-alerts/src/app/pcap/service/pcap.service.spec.ts
+++ b/metron-interface/metron-alerts/src/app/pcap/service/pcap.service.spec.ts
@@ -48,6 +48,10 @@ class FakeAppConfigService {
   getApiRoot() {
     return '/api/v1'
   }
+
+  getLoginPath() {
+    return '/login'
+  }
 }
 
 describe('PcapService', () => {
diff --git a/metron-interface/metron-alerts/src/app/service/app-config.service.ts b/metron-interface/metron-alerts/src/app/service/app-config.service.ts
index 0445f35..a3b7414 100644
--- a/metron-interface/metron-alerts/src/app/service/app-config.service.ts
+++ b/metron-interface/metron-alerts/src/app/service/app-config.service.ts
@@ -22,7 +22,7 @@ import { HttpClient } from '@angular/common/http';
 @Injectable()
 export class AppConfigService {
 
-  private appConfig;
+  private static appConfigStatic;
 
   constructor(private http: HttpClient) { }
 
@@ -31,11 +31,19 @@ export class AppConfigService {
             // APP_INITIALIZER only supports promises
             .toPromise()
             .then(data => {
-              this.appConfig = data;
+              AppConfigService.appConfigStatic = data;
             });
   }
 
   getApiRoot() {
-    return this.appConfig['apiRoot'];
+    return AppConfigService.appConfigStatic['apiRoot'];
+  }
+
+  getLoginPath() {
+    return AppConfigService.appConfigStatic['loginPath'];
+  }
+
+  static getAppConfigStatic() {
+    return AppConfigService.appConfigStatic;
   }
 }
\ No newline at end of file
diff --git a/metron-interface/metron-alerts/src/app/service/authentication.service.ts b/metron-interface/metron-alerts/src/app/service/authentication.service.ts
index 7739be1..f54dee2 100644
--- a/metron-interface/metron-alerts/src/app/service/authentication.service.ts
+++ b/metron-interface/metron-alerts/src/app/service/authentication.service.ts
@@ -23,14 +23,15 @@ import { ReplaySubject } from 'rxjs';
 import { GlobalConfigService } from './global-config.service';
 import { DataSource } from './data-source';
 import { AppConfigService } from './app-config.service';
+import {HttpUtil} from "../utils/httpUtil";
 
 @Injectable()
 export class AuthenticationService {
 
   private static USER_NOT_VERIFIED = 'USER-NOT-VERIFIED';
   private currentUser: string = AuthenticationService.USER_NOT_VERIFIED;
-  loginUrl = this.appConfigService.getApiRoot() + '/user';
-  logoutUrl = '/logout';
+  userUrl = this.appConfigService.getApiRoot() + '/user';
+  logoutUrl = this.appConfigService.getApiRoot() + '/logout';
   onLoginEvent: ReplaySubject<boolean> = new ReplaySubject<boolean>();
   $onLoginEvent = this.onLoginEvent.asObservable();
 
@@ -71,24 +72,23 @@ export class AuthenticationService {
 
   public logout(): void {
     this.http.post(this.logoutUrl, {}).subscribe(response => {
-        this.currentUser = AuthenticationService.USER_NOT_VERIFIED;
-        this.onLoginEvent.next(false);
-        this.router.navigateByUrl('/login');
+        this.clearAuthentication();
+        HttpUtil.navigateToLogin();
       },
       error => {
         console.log('Logout failed:', error);
-        this.router.navigateByUrl('/login');
+        HttpUtil.navigateToLogin();
       });
   }
 
   public checkAuthentication() {
     if (!this.isAuthenticated()) {
-      this.router.navigateByUrl('/login');
+      HttpUtil.navigateToLogin();
     }
   }
 
   public getCurrentUser(options?: {}) {
-    return this.http.get(this.loginUrl, options);
+    return this.http.get(this.userUrl, options);
   }
 
   public getCurrentUserName(): string {
@@ -102,4 +102,9 @@ export class AuthenticationService {
   public isAuthenticated(): boolean {
     return this.currentUser !== AuthenticationService.USER_NOT_VERIFIED && this.currentUser != null;
   }
+
+  public clearAuthentication(): void {
+    this.currentUser = AuthenticationService.USER_NOT_VERIFIED;
+    this.onLoginEvent.next(false);
+  }
 }
diff --git a/metron-interface/metron-alerts/src/app/shared/auth-guard.ts b/metron-interface/metron-alerts/src/app/shared/auth-guard.ts
index 3271853..7db9f9b 100644
--- a/metron-interface/metron-alerts/src/app/shared/auth-guard.ts
+++ b/metron-interface/metron-alerts/src/app/shared/auth-guard.ts
@@ -24,11 +24,13 @@ import {
 } from '@angular/router';
 import { Observable } from 'rxjs';
 import { AuthenticationService } from '../service/authentication.service';
+import {AppConfigService} from "../service/app-config.service";
+import {HttpUtil} from "../utils/httpUtil";
 
 @Injectable()
 export class AuthGuard implements CanActivate {
 
-  constructor(private authService: AuthenticationService, private router: Router) {}
+  constructor(private authService: AuthenticationService, private router: Router, private appConfigService: AppConfigService) {}
 
   canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
     if (!this.authService.isAuthenticationChecked()) {
@@ -40,7 +42,7 @@ export class AuthGuard implements CanActivate {
           } else {
             observer.next(false);
             observer.complete();
-            this.router.navigateByUrl('/login');
+            HttpUtil.navigateToLogin();
           }
         });
       });
diff --git a/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts b/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts
index eb69b56..6af5bad 100644
--- a/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts
+++ b/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts
@@ -38,7 +38,7 @@ export class AlertSearchDirective implements AfterViewInit, OnChanges {
     const el = elementRef.nativeElement;
     el.classList.add('editor');
 
-    ace.config.set('basePath', '/assets/ace');
+    ace.config.set('basePath', 'assets/ace');
 
     this.editor = ace.edit(el);
     this.editor.$blockScrolling = Infinity;
diff --git a/metron-interface/metron-alerts/src/app/shared/login-guard.ts b/metron-interface/metron-alerts/src/app/shared/login-guard.ts
index d8b8f0d..414f590 100644
--- a/metron-interface/metron-alerts/src/app/shared/login-guard.ts
+++ b/metron-interface/metron-alerts/src/app/shared/login-guard.ts
@@ -32,7 +32,7 @@ export class LoginGuard implements CanActivate {
 
   canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
 
-    this.authService.logout();
+    this.authService.clearAuthentication();
 
     return true;
 
diff --git a/metron-interface/metron-alerts/src/app/utils/httpUtil.ts b/metron-interface/metron-alerts/src/app/utils/httpUtil.ts
index 6c99490..ffbd474 100644
--- a/metron-interface/metron-alerts/src/app/utils/httpUtil.ts
+++ b/metron-interface/metron-alerts/src/app/utils/httpUtil.ts
@@ -19,6 +19,7 @@
 import {HttpErrorResponse} from '@angular/common/http';
 import {RestError} from '../model/rest-error';
 import {throwError as observableThrowError, Observable} from 'rxjs';
+import {AppConfigService} from "../service/app-config.service";
 
 export class HttpUtil {
 
@@ -37,7 +38,7 @@ export class HttpUtil {
     // We'd also dig deeper into the error to get a better message
     let restError: RestError;
     if (res.status === 401) {
-      window.location.assign('/login?sessionExpired=true');
+      HttpUtil.navigateToLogin();
     } else if (res.status !== 404) {
       restError = res;
     } else {
@@ -46,4 +47,9 @@ export class HttpUtil {
     }
     return observableThrowError(restError);
   }
+
+  public static navigateToLogin() {
+    let loginPath = AppConfigService.getAppConfigStatic()['loginPath'];
+    location.href = loginPath;
+  }
 }
diff --git a/metron-interface/metron-alerts/src/assets/app-config.json b/metron-interface/metron-alerts/src/assets/app-config.json
index 7fa734d..e485071 100644
--- a/metron-interface/metron-alerts/src/assets/app-config.json
+++ b/metron-interface/metron-alerts/src/assets/app-config.json
@@ -1,3 +1,4 @@
 {
-  "apiRoot": "/api/v1"
+  "apiRoot": "/api/v1",
+  "loginPath": "/login"
 }
\ No newline at end of file
diff --git a/metron-interface/metron-alerts/src/index.html b/metron-interface/metron-alerts/src/index.html
index e051e27..fef547c 100644
--- a/metron-interface/metron-alerts/src/index.html
+++ b/metron-interface/metron-alerts/src/index.html
@@ -16,7 +16,8 @@
 <head>
   <meta charset="utf-8">
   <title>Metron Alerts</title>
-  <base href="/">
+  <!-- The base href is relative so that static assets are properly resolved when behind a reverse proxy -->
+  <base href="./">
 
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
diff --git a/metron-interface/metron-config/src/app/_fonts.scss b/metron-interface/metron-config/src/app/_fonts.scss
index fdcb87e..a3a84b8 100644
--- a/metron-interface/metron-config/src/app/_fonts.scss
+++ b/metron-interface/metron-config/src/app/_fonts.scss
@@ -19,83 +19,83 @@
   font-family: 'Roboto';
   font-style: normal;
   font-weight: 100;
-  src: local('Roboto Thin'), local('Roboto-Thin'), url("/assets/fonts/Roboto/Roboto-Thin.ttf");
+  src: local('Roboto Thin'), local('Roboto-Thin'), url("assets/fonts/Roboto/Roboto-Thin.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: normal;
   font-weight: 300;
-  src: local('Roboto Light'), local('Roboto-Light'), url("/assets/fonts/Roboto/Roboto-Light.ttf");
+  src: local('Roboto Light'), local('Roboto-Light'), url("assets/fonts/Roboto/Roboto-Light.ttf");
 }
 
 @font-face {
   font-family: 'Roboto-Regular';
   font-style: normal;
   font-weight: 400;
-  src: local('Roboto'), local('Roboto-Regular'), url("/assets/fonts/Roboto/Roboto-Regular.ttf");
+  src: local('Roboto'), local('Roboto-Regular'), url("assets/fonts/Roboto/Roboto-Regular.ttf");
 }
 
 @font-face {
   font-family: 'Roboto-Medium';
   font-style: normal;
   font-weight: 500;
-  src: local('Roboto Medium'), local('Roboto-Medium'), url("/assets/fonts/Roboto/Roboto-Medium.ttf");
+  src: local('Roboto Medium'), local('Roboto-Medium'), url("assets/fonts/Roboto/Roboto-Medium.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: normal;
   font-weight: 700;
-  src: local('Roboto Bold'), local('Roboto-Bold'), url("/assets/fonts/Roboto/Roboto-Bold.ttf");
+  src: local('Roboto Bold'), local('Roboto-Bold'), url("assets/fonts/Roboto/Roboto-Bold.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: normal;
   font-weight: 900;
-  src: local('Roboto Black'), local('Roboto-Black'), url("/assets/fonts/Roboto/Roboto-Black.ttf");
+  src: local('Roboto Black'), local('Roboto-Black'), url("assets/fonts/Roboto/Roboto-Black.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: italic;
   font-weight: 100;
-  src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), url("/assets/fonts/Roboto/Roboto-ThinItalic.ttf");
+  src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), url("assets/fonts/Roboto/Roboto-ThinItalic.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: italic;
   font-weight: 300;
-  src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url("/assets/fonts/Roboto/Roboto-LightItalic.ttf");
+  src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url("assets/fonts/Roboto/Roboto-LightItalic.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: italic;
   font-weight: 400;
-  src: local('Roboto Italic'), local('Roboto-Italic'), url("/assets/fonts/Roboto/Roboto-Italic.ttf");
+  src: local('Roboto Italic'), local('Roboto-Italic'), url("assets/fonts/Roboto/Roboto-Italic.ttf");
 }
 
 @font-face {
   font-family: 'Roboto-MediumItalic';
   font-style: italic;
   font-weight: 500;
-  src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), url("/assets/fonts/Roboto/Roboto-MediumItalic.ttf");
+  src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), url("assets/fonts/Roboto/Roboto-MediumItalic.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: italic;
   font-weight: 700;
-  src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url("/assets/fonts/Roboto/Roboto-BoldItalic.ttf");
+  src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url("assets/fonts/Roboto/Roboto-BoldItalic.ttf");
 }
 
 @font-face {
   font-family: 'Roboto';
   font-style: italic;
   font-weight: 900;
-  src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), url("/assets/fonts/Roboto/Roboto-BlackItalic.ttf");
+  src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), url("assets/fonts/Roboto/Roboto-BlackItalic.ttf");
 }
 
diff --git a/metron-interface/metron-config/src/app/app.component.spec.ts b/metron-interface/metron-config/src/app/app.component.spec.ts
index 4ca1bfa..c5be76a 100644
--- a/metron-interface/metron-config/src/app/app.component.spec.ts
+++ b/metron-interface/metron-config/src/app/app.component.spec.ts
@@ -22,19 +22,11 @@ import { Observable } from 'rxjs';
 import { AppComponent } from './app.component';
 import { AuthenticationService } from './service/authentication.service';
 import { AppModule } from './app.module';
-import { APP_CONFIG, METRON_REST_CONFIG } from './app.config';
-import { IAppConfig } from './app.config.interface';
 import { HttpResponse, HttpClient } from '@angular/common/http';
+import {AppConfigService} from './service/app-config.service';
+import {MockAppConfigService} from './service/mock.app-config.service';
 
 class MockAuthenticationService extends AuthenticationService {
-  constructor(
-    private http2: HttpClient,
-    private router2: Router,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, router2, config2);
-    this.onLoginEvent.next(false);
-  }
 
   public checkAuthentication() {}
 
@@ -61,7 +53,7 @@ describe('App: Static', () => {
       providers: [
         { provide: AuthenticationService, useClass: MockAuthenticationService },
         { provide: Router, useClass: MockRouter },
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     fixture = TestBed.createComponent(AppComponent);
@@ -74,7 +66,6 @@ describe('App: Static', () => {
   });
 
   it('should return true/false from loginevent and loggedIn should be set', () => {
-    expect(comp.loggedIn).toEqual(false);
     authenticationService.onLoginEvent.next(true);
     expect(comp.loggedIn).toEqual(true);
     authenticationService.onLoginEvent.next(false);
diff --git a/metron-interface/metron-config/src/app/app.module.ts b/metron-interface/metron-config/src/app/app.module.ts
index 8df5293..89aa937 100644
--- a/metron-interface/metron-config/src/app/app.module.ts
+++ b/metron-interface/metron-config/src/app/app.module.ts
@@ -15,7 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {NgModule} from '@angular/core';
+import {APP_INITIALIZER, NgModule} from '@angular/core';
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {BrowserModule} from '@angular/platform-browser';
 import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http';
@@ -45,17 +45,23 @@ import {SensorParserConfigHistoryService} from './service/sensor-parser-config-h
 import {SensorIndexingConfigService} from './service/sensor-indexing-config.service';
 import {HdfsService} from './service/hdfs.service';
 import { DefaultHeadersInterceptor } from './http-interceptors/default-headers.interceptor';
+import {AppConfigService} from './service/app-config.service';
 
+export function initConfig(appConfigService: AppConfigService) {
+  return () => appConfigService.loadAppConfig();
+}
 
 @NgModule({
   imports: [ BrowserModule, FormsModule, ReactiveFormsModule, HttpClientModule, SensorParserListModule,
     SensorParserConfigModule, SensorParserConfigReadonlyModule, GeneralSettingsModule, MetronConfigRoutingModule ],
   declarations: [ AppComponent, NavbarComponent, VerticalNavbarComponent ],
-  providers: [  AuthenticationService, AuthGuard, LoginGuard, SensorParserConfigService,
+  providers: [  AppConfigService, AuthenticationService, AuthGuard, LoginGuard, SensorParserConfigService,
     SensorParserConfigHistoryService, SensorEnrichmentConfigService, SensorIndexingConfigService,
     StormService, KafkaService, GrokValidationService, StellarService, HdfsService,
-    GlobalConfigService, MetronAlerts, MetronDialogBox, { provide: APP_CONFIG, useValue: METRON_REST_CONFIG },
-    { provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true }],
+    GlobalConfigService, MetronAlerts, MetronDialogBox,
+    { provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true },
+    { provide: APP_INITIALIZER, useFactory: initConfig, deps: [AppConfigService], multi: true }
+    ],
   bootstrap:    [ AppComponent ]
 })
 export class AppModule {
diff --git a/metron-interface/metron-config/src/app/app.routes.ts b/metron-interface/metron-config/src/app/app.routes.ts
index fd3655d..fd04063 100644
--- a/metron-interface/metron-config/src/app/app.routes.ts
+++ b/metron-interface/metron-config/src/app/app.routes.ts
@@ -21,7 +21,7 @@ import {AuthGuard} from './shared/auth-guard';
 import {LoginGuard} from './shared/login-guard';
 
 export const routes: Routes = [
-  { path: '',  redirectTo: 'sensors', canActivate: [AuthGuard], pathMatch: 'full'},
+  { path: '',  redirectTo: 'sensors', pathMatch: 'full'},
   { path: 'login', loadChildren: 'app/login/login.module#LoginModule', canActivate: [LoginGuard] },
   { path: 'sensors', loadChildren: 'app/sensors/sensor-parser-list/sensor-parser-list.module#SensorParserListModule',
     canActivate: [AuthGuard] },
diff --git a/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts b/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts
index b1c5ede..d517565 100644
--- a/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts
+++ b/metron-interface/metron-config/src/app/general-settings/general-settings.component.spec.ts
@@ -24,20 +24,13 @@ import { MetronDialogBox } from '../shared/metron-dialog-box';
 import { GlobalConfigService } from '../service/global-config.service';
 import { GeneralSettingsModule } from './general-settings.module';
 import { Observable, throwError } from 'rxjs';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../app.config';
-import { IAppConfig } from '../app.config.interface';
+import {AppConfigService} from '../service/app-config.service';
+import {MockAppConfigService} from '../service/mock.app-config.service';
 
 class MockGlobalConfigService extends GlobalConfigService {
   _config: any = {};
   _postSuccess = true;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public post(globalConfig: {}): Observable<{}> {
     if (this._postSuccess) {
       return Observable.create(observer => {
@@ -94,7 +87,7 @@ describe('GeneralSettingsComponent', () => {
         MetronAlerts,
         MetronDialogBox,
         { provide: GlobalConfigService, useClass: MockGlobalConfigService },
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     fixture = TestBed.createComponent(GeneralSettingsComponent);
diff --git a/metron-interface/metron-config/src/app/navbar/navbar.component.ts b/metron-interface/metron-config/src/app/navbar/navbar.component.ts
index 7861423..0deda66 100644
--- a/metron-interface/metron-config/src/app/navbar/navbar.component.ts
+++ b/metron-interface/metron-config/src/app/navbar/navbar.component.ts
@@ -18,6 +18,7 @@
 import { Component, OnInit, OnDestroy } from '@angular/core';
 import { AuthenticationService } from '../service/authentication.service';
 import { Subscription } from 'rxjs';
+import {HttpHeaders} from "@angular/common/http";
 
 @Component({
   selector: 'metron-config-navbar',
@@ -32,7 +33,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.authService = this.authenticationService
-      .getCurrentUser({ responseType: 'text' })
+      .getCurrentUser({ headers: new HttpHeaders({'Accept': 'text/plain'}), responseType: 'text'})
       .subscribe(r => {
         this.currentUser = r;
       });
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
index d744481..1a027f5 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
@@ -33,8 +33,6 @@ import { StormService } from '../../service/storm.service';
 import { MetronAlerts } from '../../shared/metron-alerts';
 import { FieldTransformer } from '../../model/field-transformer';
 import { SensorParserConfigReadonlyModule } from './sensor-parser-config-readonly.module';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../../app.config';
-import { IAppConfig } from '../../app.config.interface';
 import { SensorEnrichmentConfigService } from '../../service/sensor-enrichment-config.service';
 import {
   SensorEnrichmentConfig,
@@ -44,6 +42,8 @@ import {
 import { HdfsService } from '../../service/hdfs.service';
 import { GrokValidationService } from '../../service/grok-validation.service';
 import { RiskLevelRule } from '../../model/risk-level-rule';
+import {AppConfigService} from '../../service/app-config.service';
+import {MockAppConfigService} from '../../service/mock.app-config.service';
 
 class MockRouter {
   navigateByUrl(url: string) {}
@@ -63,13 +63,6 @@ class MockActivatedRoute {
 }
 
 class MockAuthenticationService extends AuthenticationService {
-  constructor(
-    private http2: HttpClient,
-    private router2: Router,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, router2, config2);
-  }
 
   public getCurrentUser(options): Observable<{}> {
     let response: { body: 'user' };
@@ -83,13 +76,6 @@ class MockAuthenticationService extends AuthenticationService {
 class MockSensorParserConfigHistoryService extends SensorParserConfigHistoryService {
   private sensorParserConfigHistory: SensorParserConfigHistory;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setForTest(sensorParserConfigHistory: SensorParserConfigHistory) {
     this.sensorParserConfigHistory = sensorParserConfigHistory;
   }
@@ -103,24 +89,11 @@ class MockSensorParserConfigHistoryService extends SensorParserConfigHistoryServ
 }
 
 class MockSensorParserConfigService extends SensorParserConfigService {
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
 }
 
 class MockStormService extends StormService {
   private topologyStatus: TopologyStatus;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setForTest(topologyStatus: TopologyStatus) {
     this.topologyStatus = topologyStatus;
   }
@@ -134,12 +107,6 @@ class MockStormService extends StormService {
 }
 
 class MockGrokValidationService extends GrokValidationService {
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
 
   public list(): Observable<string[]> {
     return Observable.create(observer => {
@@ -161,13 +128,6 @@ class MockGrokValidationService extends GrokValidationService {
 class MockKafkaService extends KafkaService {
   private kafkaTopic: KafkaTopic;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setForTest(kafkaTopic: KafkaTopic) {
     this.kafkaTopic = kafkaTopic;
   }
@@ -191,13 +151,6 @@ class MockHdfsService extends HdfsService {
   private fileList: string[];
   private contents: string;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setContents(contents: string) {
     this.contents = contents;
   }
@@ -297,7 +250,7 @@ describe('Component: SensorParserConfigReadonly', () => {
         { provide: HdfsService, useClass: MockHdfsService },
         { provide: GrokValidationService, useClass: MockGrokValidationService },
         { provide: Router, useClass: MockRouter },
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG },
+        { provide: AppConfigService, useClass: MockAppConfigService },
         MetronAlerts
       ]
     });
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
index 06927f4..09809a2 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
@@ -39,14 +39,14 @@ import { FieldTransformer } from '../../model/field-transformer';
 import { SensorParserConfigModule } from './sensor-parser-config.module';
 import { SensorEnrichmentConfigService } from '../../service/sensor-enrichment-config.service';
 import { SensorEnrichmentConfig } from '../../model/sensor-enrichment-config';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../../app.config';
-import { IAppConfig } from '../../app.config.interface';
 import { SensorIndexingConfigService } from '../../service/sensor-indexing-config.service';
 import { IndexingConfigurations } from '../../model/sensor-indexing-config';
 import { of } from 'rxjs';
 import { HdfsService } from '../../service/hdfs.service';
 import { RestError } from '../../model/rest-error';
 import { RiskLevelRule } from '../../model/risk-level-rule';
+import {AppConfigService} from '../../service/app-config.service';
+import {MockAppConfigService} from '../../service/mock.app-config.service';
 
 class MockRouter {
   navigateByUrl(url: string) {}
@@ -72,13 +72,6 @@ class MockSensorParserConfigService extends SensorParserConfigService {
   private postedSensorParserConfig: SensorParserConfig;
   private throwError: boolean;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public post(
     name: string,
     sensorParserConfig: SensorParserConfig
@@ -157,13 +150,6 @@ class MockSensorIndexingConfigService extends SensorIndexingConfigService {
   private sensorIndexingConfig: IndexingConfigurations;
   private throwError: boolean;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public post(
     name: string,
     sensorIndexingConfig: IndexingConfigurations
@@ -214,13 +200,6 @@ class MockKafkaService extends KafkaService {
   private kafkaTopicForPost: KafkaTopic;
   private sampleData = { key1: 'value1', key2: 'value2' };
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setKafkaTopic(name: string, kafkaTopic: KafkaTopic) {
     this.name = name;
     this.kafkaTopic = kafkaTopic;
@@ -274,13 +253,6 @@ class MockGrokValidationService extends GrokValidationService {
   private path: string;
   private contents: string;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setContents(path: string, contents: string) {
     this.path = path;
     this.contents = contents;
@@ -321,13 +293,6 @@ class MockHdfsService extends HdfsService {
   private contents: string;
   private postedContents: string;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setFileList(path: string, fileList: string[]) {
     this.path = path;
     this.fileList = fileList;
@@ -388,13 +353,6 @@ class MockHdfsService extends HdfsService {
 }
 
 class MockAuthenticationService extends AuthenticationService {
-  constructor(
-    private http2: HttpClient,
-    private router2: Router,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, router2, config2);
-  }
 
   public getCurrentUser(options: {}): Observable<HttpResponse<{}>> {
     let responseOptions: {} = { body: 'user' };
@@ -407,13 +365,6 @@ class MockTransformationValidationService extends StellarService {
   private transformationValidationResult: any;
   private transformationValidationForValidate: SensorParserContext;
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setTransformationValidationResultForTest(
     transformationValidationResult: any
   ): void {
@@ -586,7 +537,7 @@ describe('Component: SensorParserConfig', () => {
           provide: SensorEnrichmentConfigService,
           useClass: MockSensorEnrichmentConfigService
         },
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     fixture = TestBed.createComponent(SensorParserConfigComponent);
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts
index ec07cbb..eb4dbff 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.spec.ts
@@ -35,19 +35,11 @@ import { Sort } from '../../util/enums';
 import 'jquery';
 import { SensorParserConfigHistoryService } from '../../service/sensor-parser-config-history.service';
 import { SensorParserConfigHistory } from '../../model/sensor-parser-config-history';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../../app.config';
 import { StormService } from '../../service/storm.service';
-import { IAppConfig } from '../../app.config.interface';
+import {AppConfigService} from '../../service/app-config.service';
+import {MockAppConfigService} from '../../service/mock.app-config.service';
 
 class MockAuthenticationService extends AuthenticationService {
-  constructor(
-    private http2: HttpClient,
-    private router2: Router,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, router2, config2);
-  }
-
   public checkAuthentication() {}
 
   public getCurrentUser(options: {}): Observable<HttpResponse<{}>> {
@@ -61,13 +53,6 @@ class MockAuthenticationService extends AuthenticationService {
 class MockSensorParserConfigHistoryService extends SensorParserConfigHistoryService {
   private allSensorParserConfigHistory: SensorParserConfigHistory[];
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setSensorParserConfigHistoryForTest(
     allSensorParserConfigHistory: SensorParserConfigHistory[]
   ) {
@@ -85,13 +70,6 @@ class MockSensorParserConfigHistoryService extends SensorParserConfigHistoryServ
 class MockSensorParserConfigService extends SensorParserConfigService {
   private sensorParserConfigs: {};
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setSensorParserConfigForTest(sensorParserConfigs: {}) {
     this.sensorParserConfigs = sensorParserConfigs;
   }
@@ -124,13 +102,6 @@ class MockSensorParserConfigService extends SensorParserConfigService {
 class MockStormService extends StormService {
   private topologyStatuses: TopologyStatus[];
 
-  constructor(
-    private http2: HttpClient,
-    @Inject(APP_CONFIG) private config2: IAppConfig
-  ) {
-    super(http2, config2);
-  }
-
   public setTopologyStatusForTest(topologyStatuses: TopologyStatus[]) {
     this.topologyStatuses = topologyStatuses;
   }
@@ -198,7 +169,7 @@ describe('Component: SensorParserList', () => {
         },
         { provide: Router, useClass: MockRouter },
         { provide: MetronDialogBox, useClass: MockMetronDialogBox },
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG },
+        { provide: AppConfigService, useClass: MockAppConfigService },
         MetronAlerts
       ]
     });
diff --git a/metron-interface/metron-alerts/src/app/service/app-config.service.ts b/metron-interface/metron-config/src/app/service/app-config.service.ts
similarity index 79%
copy from metron-interface/metron-alerts/src/app/service/app-config.service.ts
copy to metron-interface/metron-config/src/app/service/app-config.service.ts
index 0445f35..6081ea2 100644
--- a/metron-interface/metron-alerts/src/app/service/app-config.service.ts
+++ b/metron-interface/metron-config/src/app/service/app-config.service.ts
@@ -22,7 +22,7 @@ import { HttpClient } from '@angular/common/http';
 @Injectable()
 export class AppConfigService {
 
-  private appConfig;
+  private static appConfigStatic;
 
   constructor(private http: HttpClient) { }
 
@@ -31,11 +31,19 @@ export class AppConfigService {
             // APP_INITIALIZER only supports promises
             .toPromise()
             .then(data => {
-              this.appConfig = data;
+              AppConfigService.appConfigStatic = data;
             });
   }
 
   getApiRoot() {
-    return this.appConfig['apiRoot'];
+    return AppConfigService.appConfigStatic['apiRoot'];
+  };
+
+  getLoginPath() {
+    return AppConfigService.appConfigStatic['loginPath'];
+  }
+
+  static getAppConfigStatic() {
+    return AppConfigService.appConfigStatic;
   }
 }
\ No newline at end of file
diff --git a/metron-interface/metron-config/src/app/service/authentication.service.spec.ts b/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
index dedb69c..7a82ac0 100644
--- a/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
@@ -23,7 +23,9 @@ import {
   HttpTestingController
 } from '@angular/common/http/testing';
 import { AuthenticationService } from './authentication.service';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../app.config';
+import {AppConfigService} from "./app-config.service";
+import {MockAppConfigService} from './mock.app-config.service';
+import {HttpUtil} from '../util/httpUtil';
 
 class MockRouter {
   navigateByUrl(url: string) {}
@@ -40,7 +42,7 @@ describe('AuthenticationService', () => {
       providers: [
         AuthenticationService,
         { provide: Router, useClass: MockRouter },
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     authenticationService = TestBed.get(AuthenticationService);
@@ -86,31 +88,31 @@ describe('AuthenticationService', () => {
     });
 
     it('logout', () => {
-      spyOn(router, 'navigateByUrl');
+      spyOn(HttpUtil, 'navigateToLogin');
       spyOn(authenticationService.onLoginEvent, 'next');
 
       authenticationService.logout();
-      const req = mockBackend.match('/logout');
+      const req = mockBackend.match('/api/v1/logout');
       const req2 = mockBackend.expectOne('/api/v1/user');
       req.map(r => r.flush(''));
       expect(req[0].request.method).toBe('POST');
 
-      expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+      expect(HttpUtil.navigateToLogin).toHaveBeenCalledWith();
       expect(authenticationService.onLoginEvent.getValue()).toEqual(false);
     });
 
     it('checkAuthentication', () => {
       let isAuthenticated = false;
-      spyOn(router, 'navigateByUrl');
+      spyOn(HttpUtil, 'navigateToLogin');
       spyOn(authenticationService, 'isAuthenticated').and.callFake(function() {
         return isAuthenticated;
       });
 
       authenticationService.checkAuthentication();
-      expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+      expect(HttpUtil.navigateToLogin).toHaveBeenCalledWith();
       isAuthenticated = true;
       authenticationService.checkAuthentication();
-      expect(router.navigateByUrl['calls'].count()).toEqual(1);
+      expect(HttpUtil.navigateToLogin['calls'].count()).toEqual(1);
       const req = mockBackend.expectOne('/api/v1/user');
     });
 
diff --git a/metron-interface/metron-config/src/app/service/authentication.service.ts b/metron-interface/metron-config/src/app/service/authentication.service.ts
index 3c4d880..f6d0040 100644
--- a/metron-interface/metron-config/src/app/service/authentication.service.ts
+++ b/metron-interface/metron-config/src/app/service/authentication.service.ts
@@ -18,28 +18,28 @@
 import { Injectable, Inject } from '@angular/core';
 import { HttpClient, HttpHeaders } from '@angular/common/http';
 import { Router } from '@angular/router';
-import { BehaviorSubject } from 'rxjs';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {BehaviorSubject, Subject} from 'rxjs';
+import {AppConfigService} from './app-config.service';
+import {HttpUtil} from '../util/httpUtil';
 
 @Injectable()
 export class AuthenticationService {
   private static USER_NOT_VERIFIED = 'USER-NOT-VERIFIED';
   private currentUser: string = AuthenticationService.USER_NOT_VERIFIED;
-  loginUrl: string = this.config.apiEndpoint + '/user';
-  logoutUrl = '/logout';
+  loginUrl: string = this.appConfigService.getApiRoot() + '/user';
+  logoutUrl = this.appConfigService.getApiRoot() + '/logout';
   onLoginEvent: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
 
   constructor(
     private http: HttpClient,
     private router: Router,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {
     this.init();
   }
 
   public init() {
-    this.getCurrentUser({ responseType: 'text' }).subscribe(
+    this.getCurrentUser({ headers: new HttpHeaders({'Accept': 'text/plain'}), responseType: 'text'}).subscribe(
       response => {
         this.currentUser = response.toString();
         if (this.currentUser) {
@@ -55,7 +55,7 @@ export class AuthenticationService {
   public login(username: string, password: string, onError): void {
     let credentials = btoa(username + ':' + password);
     this.getCurrentUser({
-      headers: new HttpHeaders({ Authorization: `Basic ${credentials}` }),
+      headers: new HttpHeaders({ Authorization: `Basic ${credentials}` , 'Accept': 'text/plain'}),
       responseType: 'text'
     }).subscribe(
       response => {
@@ -72,19 +72,19 @@ export class AuthenticationService {
   public logout(): void {
     this.http.post(this.logoutUrl, {}).subscribe(
       response => {
-        this.currentUser = AuthenticationService.USER_NOT_VERIFIED;
-        this.onLoginEvent.next(false);
-        this.router.navigateByUrl('/login');
+        this.clearAuthentication();
+        HttpUtil.navigateToLogin();
       },
       error => {
         console.log(error);
+        HttpUtil.navigateToLogin();
       }
     );
   }
 
   public checkAuthentication() {
     if (!this.isAuthenticated()) {
-      this.router.navigateByUrl('/login');
+      HttpUtil.navigateToLogin();
     }
   }
 
@@ -102,4 +102,9 @@ export class AuthenticationService {
       this.currentUser != null
     );
   }
+
+  public clearAuthentication(): void {
+    this.currentUser = AuthenticationService.USER_NOT_VERIFIED;
+    this.onLoginEvent.next(false);
+  }
 }
diff --git a/metron-interface/metron-config/src/app/service/global-config.service.spec.ts b/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
index 9ad3b13..1bd3ef8 100644
--- a/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
@@ -22,6 +22,8 @@ import {
   HttpTestingController,
   HttpClientTestingModule
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('GlobalConfigService', () => {
   let mockBackend: HttpTestingController;
@@ -32,7 +34,7 @@ describe('GlobalConfigService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         GlobalConfigService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/global-config.service.ts b/metron-interface/metron-config/src/app/service/global-config.service.ts
index 84bd014..97ae167 100644
--- a/metron-interface/metron-config/src/app/service/global-config.service.ts
+++ b/metron-interface/metron-config/src/app/service/global-config.service.ts
@@ -21,16 +21,15 @@ import { Response, ResponseOptions } from '@angular/http';
 import {Observable} from 'rxjs';
 import { map, catchError } from 'rxjs/operators';
 import {HttpUtil} from '../util/httpUtil';
-import {IAppConfig} from '../app.config.interface';
-import {APP_CONFIG} from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class GlobalConfigService {
-  url = this.config.apiEndpoint + '/global/config';
+  url = this.appConfigService.getApiRoot() + '/global/config';
 
   private globalConfig = {};
 
-  constructor(private http: HttpClient, @Inject(APP_CONFIG) private config: IAppConfig) {
+  constructor(private http: HttpClient, private appConfigService: AppConfigService) {
     this.globalConfig['solr.collection'] = 'metron';
     this.globalConfig['storm.indexingWorkers'] = 1;
     this.globalConfig['storm.indexingExecutors'] = 2;
diff --git a/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts b/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
index d66b0aa..bed2268 100644
--- a/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
@@ -18,11 +18,12 @@
 import { async, inject, TestBed } from '@angular/core/testing';
 import { GrokValidationService } from './grok-validation.service';
 import { GrokValidation } from '../model/grok-validation';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../app.config';
 import {
   HttpClientTestingModule,
   HttpTestingController
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('GrokValidationService', () => {
   let grokValidationService: GrokValidationService;
@@ -33,7 +34,7 @@ describe('GrokValidationService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         GrokValidationService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     grokValidationService = TestBed.get(GrokValidationService);
diff --git a/metron-interface/metron-config/src/app/service/grok-validation.service.ts b/metron-interface/metron-config/src/app/service/grok-validation.service.ts
index 7da8798..1e61b3e 100644
--- a/metron-interface/metron-config/src/app/service/grok-validation.service.ts
+++ b/metron-interface/metron-config/src/app/service/grok-validation.service.ts
@@ -21,16 +21,15 @@ import { Observable } from 'rxjs';
 import { map, catchError } from 'rxjs/operators';
 import { GrokValidation } from '../model/grok-validation';
 import { HttpUtil } from '../util/httpUtil';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class GrokValidationService {
-  url = this.config.apiEndpoint + '/grok';
+  url = this.appConfigService.getApiRoot() + '/grok';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public validate(grokValidation: GrokValidation): Observable<GrokValidation> {
diff --git a/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts b/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
index 251d392..9163e29 100644
--- a/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
@@ -23,6 +23,8 @@ import {
   HttpTestingController,
   HttpClientTestingModule
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('HdfsService', () => {
   let hdfsService: HdfsService;
@@ -33,7 +35,7 @@ describe('HdfsService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         HdfsService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     hdfsService = TestBed.get(HdfsService);
diff --git a/metron-interface/metron-config/src/app/service/hdfs.service.ts b/metron-interface/metron-config/src/app/service/hdfs.service.ts
index fdeb001..54e7369 100644
--- a/metron-interface/metron-config/src/app/service/hdfs.service.ts
+++ b/metron-interface/metron-config/src/app/service/hdfs.service.ts
@@ -20,16 +20,15 @@ import { HttpClient, HttpParams } from '@angular/common/http';
 import { Observable } from 'rxjs';
 import { map, catchError } from 'rxjs/operators';
 import { HttpUtil } from '../util/httpUtil';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class HdfsService {
-  url = this.config.apiEndpoint + '/hdfs';
+  url = this.appConfigService.getApiRoot() + '/hdfs';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public list(path: string): Observable<string[]> {
diff --git a/metron-interface/metron-config/src/app/service/kafka.service.spec.ts b/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
index 12f0c12..cc7269f 100644
--- a/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
@@ -18,11 +18,12 @@
 import { TestBed } from '@angular/core/testing';
 import { KafkaService } from './kafka.service';
 import { KafkaTopic } from '../model/kafka-topic';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../app.config';
 import {
   HttpClientTestingModule,
   HttpTestingController
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('KafkaService', () => {
   let mockBackend: HttpTestingController;
@@ -33,7 +34,7 @@ describe('KafkaService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         KafkaService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/kafka.service.ts b/metron-interface/metron-config/src/app/service/kafka.service.ts
index f39f364..62a95a0 100644
--- a/metron-interface/metron-config/src/app/service/kafka.service.ts
+++ b/metron-interface/metron-config/src/app/service/kafka.service.ts
@@ -21,17 +21,16 @@ import { Observable } from 'rxjs';
 import { map, catchError } from 'rxjs/operators';
 import { KafkaTopic } from '../model/kafka-topic';
 import { HttpUtil } from '../util/httpUtil';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
 import { RestError } from '../model/rest-error';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class KafkaService {
-  url = this.config.apiEndpoint + '/kafka/topic';
+  url = this.appConfigService.getApiRoot() + '/kafka/topic';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public post(kafkaTopic: KafkaTopic): Observable<KafkaTopic> {
diff --git a/metron-interface/metron-alerts/src/app/service/app-config.service.ts b/metron-interface/metron-config/src/app/service/mock.app-config.service.ts
similarity index 63%
copy from metron-interface/metron-alerts/src/app/service/app-config.service.ts
copy to metron-interface/metron-config/src/app/service/mock.app-config.service.ts
index 0445f35..66f41e4 100644
--- a/metron-interface/metron-alerts/src/app/service/app-config.service.ts
+++ b/metron-interface/metron-config/src/app/service/mock.app-config.service.ts
@@ -15,27 +15,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {AppConfigService} from "./app-config.service";
 
-import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
+export class MockAppConfigService extends AppConfigService {
 
-@Injectable()
-export class AppConfigService {
-
-  private appConfig;
-
-  constructor(private http: HttpClient) { }
-
-  loadAppConfig() {
-    return this.http.get('assets/app-config.json')
-            // APP_INITIALIZER only supports promises
-            .toPromise()
-            .then(data => {
-              this.appConfig = data;
-            });
+  getApiRoot() {
+    return '/api/v1'
   }
 
-  getApiRoot() {
-    return this.appConfig['apiRoot'];
+  getLoginPath() {
+    return '/login'
   }
 }
\ No newline at end of file
diff --git a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
index e2cb131..3bc9a98 100644
--- a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
@@ -27,6 +27,8 @@ import {
   HttpClientTestingModule,
   HttpTestingController
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('SensorEnrichmentConfigService', () => {
   let mockBackend: HttpTestingController;
@@ -37,7 +39,7 @@ describe('SensorEnrichmentConfigService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         SensorEnrichmentConfigService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
index bc26581..d56591d 100644
--- a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
@@ -21,16 +21,15 @@ import { Observable } from 'rxjs';
 import { map, catchError } from 'rxjs/operators';
 import { SensorEnrichmentConfig } from '../model/sensor-enrichment-config';
 import { HttpUtil } from '../util/httpUtil';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class SensorEnrichmentConfigService {
-  url = this.config.apiEndpoint + '/sensor/enrichment/config';
+  url = this.appConfigService.getApiRoot() + '/sensor/enrichment/config';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public post(
diff --git a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
index e2c1568..0ab89cc 100644
--- a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
@@ -17,13 +17,14 @@
  */
 import { TestBed } from '@angular/core/testing';
 import { HttpResponse } from '@angular/common/http';
-import { METRON_REST_CONFIG, APP_CONFIG } from '../app.config';
 import { SensorIndexingConfigService } from './sensor-indexing-config.service';
 import { IndexingConfigurations } from '../model/sensor-indexing-config';
 import {
   HttpClientTestingModule,
   HttpTestingController
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('SensorIndexingConfigService', () => {
   let mockBackend: HttpTestingController;
@@ -34,7 +35,7 @@ describe('SensorIndexingConfigService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         SensorIndexingConfigService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts
index 41e2c00..920a958 100644
--- a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.ts
@@ -22,16 +22,15 @@ import { map, catchError } from 'rxjs/operators';
 
 import { IndexingConfigurations } from '../model/sensor-indexing-config';
 import { HttpUtil } from '../util/httpUtil';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class SensorIndexingConfigService {
-  url = this.config.apiEndpoint + '/sensor/indexing/config';
+  url = this.appConfigService.getApiRoot() + '/sensor/indexing/config';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public post(
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts
index 2170a59..4816ae5 100644
--- a/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.spec.ts
@@ -17,13 +17,14 @@
  */
 import { TestBed } from '@angular/core/testing';
 import { SensorParserConfig } from '../model/sensor-parser-config';
-import { METRON_REST_CONFIG, APP_CONFIG } from '../app.config';
 import { SensorParserConfigHistoryService } from './sensor-parser-config-history.service';
 import { SensorParserConfigHistory } from '../model/sensor-parser-config-history';
 import {
   HttpTestingController,
   HttpClientTestingModule
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('SensorParserConfigHistoryService', () => {
   let mockBackend: HttpTestingController;
@@ -34,7 +35,7 @@ describe('SensorParserConfigHistoryService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         SensorParserConfigHistoryService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts
index a7a73b2..c5d0c67 100644
--- a/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config-history.service.ts
@@ -20,19 +20,18 @@ import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 import { map, catchError } from 'rxjs/operators';
 import { HttpUtil } from '../util/httpUtil';
-import { IAppConfig } from '../app.config.interface';
 import { SensorParserConfigHistory } from '../model/sensor-parser-config-history';
-import { APP_CONFIG } from '../app.config';
 import { SensorParserConfig } from '../model/sensor-parser-config';
 import { RestError } from '../model/rest-error';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class SensorParserConfigHistoryService {
-  url = this.config.apiEndpoint + '/sensor/parser/config';
+  url = this.appConfigService.getApiRoot() + '/sensor/parser/config';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public get(name: string): Observable<RestError | SensorParserConfigHistory> {
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts
index 4f2750e..9baf883 100644
--- a/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.spec.ts
@@ -24,6 +24,8 @@ import {
   HttpClientTestingModule,
   HttpTestingController
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('SensorParserConfigService', () => {
   let mockBackend: HttpTestingController;
@@ -34,7 +36,7 @@ describe('SensorParserConfigService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         SensorParserConfigService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts
index b7107d3..add7cb5 100644
--- a/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts
+++ b/metron-interface/metron-config/src/app/service/sensor-parser-config.service.ts
@@ -23,12 +23,11 @@ import { SensorParserConfig } from '../model/sensor-parser-config';
 import { HttpUtil } from '../util/httpUtil';
 import { ParseMessageRequest } from '../model/parse-message-request';
 import { RestError } from '../model/rest-error';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class SensorParserConfigService {
-  url = this.config.apiEndpoint + '/sensor/parser/config';
+  url = this.appConfigService.getApiRoot() + '/sensor/parser/config';
   selectedSensorParserConfig: SensorParserConfig;
 
   dataChangedSource = new Subject<string[]>();
@@ -36,7 +35,7 @@ export class SensorParserConfigService {
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public post(
diff --git a/metron-interface/metron-config/src/app/service/stellar.service.spec.ts b/metron-interface/metron-config/src/app/service/stellar.service.spec.ts
index da329dc..ce9f34f 100644
--- a/metron-interface/metron-config/src/app/service/stellar.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/stellar.service.spec.ts
@@ -19,12 +19,13 @@ import { TestBed } from '@angular/core/testing';
 import { StellarService } from './stellar.service';
 import { SensorParserContext } from '../model/sensor-parser-context';
 import { SensorParserConfig } from '../model/sensor-parser-config';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../app.config';
 import {
   HttpClientTestingModule,
   HttpTestingController
 } from '@angular/common/http/testing';
 import { StellarFunctionDescription } from '../model/stellar-function-description';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('StellarService', () => {
   let mockBackend: HttpTestingController;
@@ -35,7 +36,7 @@ describe('StellarService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         StellarService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/stellar.service.ts b/metron-interface/metron-config/src/app/service/stellar.service.ts
index 97cb019..23ecd5f 100644
--- a/metron-interface/metron-config/src/app/service/stellar.service.ts
+++ b/metron-interface/metron-config/src/app/service/stellar.service.ts
@@ -23,16 +23,15 @@ import { map, catchError } from 'rxjs/operators';
 import { SensorParserContext } from '../model/sensor-parser-context';
 import { HttpUtil } from '../util/httpUtil';
 import { StellarFunctionDescription } from '../model/stellar-function-description';
-import { IAppConfig } from '../app.config.interface';
-import { APP_CONFIG } from '../app.config';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class StellarService {
-  url = this.config.apiEndpoint + '/stellar';
+  url = this.appConfigService.getApiRoot() + '/stellar';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public validateRules(rules: string[]): Observable<{}> {
diff --git a/metron-interface/metron-config/src/app/service/storm.service.spec.ts b/metron-interface/metron-config/src/app/service/storm.service.spec.ts
index 840aff8..6a7a5f3 100644
--- a/metron-interface/metron-config/src/app/service/storm.service.spec.ts
+++ b/metron-interface/metron-config/src/app/service/storm.service.spec.ts
@@ -18,12 +18,13 @@
 import { TestBed } from '@angular/core/testing';
 import { TopologyStatus } from '../model/topology-status';
 import { TopologyResponse } from '../model/topology-response';
-import { APP_CONFIG, METRON_REST_CONFIG } from '../app.config';
 import { StormService } from './storm.service';
 import {
   HttpTestingController,
   HttpClientTestingModule
 } from '@angular/common/http/testing';
+import {AppConfigService} from './app-config.service';
+import {MockAppConfigService} from './mock.app-config.service';
 
 describe('StormService', () => {
   let mockBackend: HttpTestingController;
@@ -34,7 +35,7 @@ describe('StormService', () => {
       imports: [HttpClientTestingModule],
       providers: [
         StormService,
-        { provide: APP_CONFIG, useValue: METRON_REST_CONFIG }
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     });
     mockBackend = TestBed.get(HttpTestingController);
diff --git a/metron-interface/metron-config/src/app/service/storm.service.ts b/metron-interface/metron-config/src/app/service/storm.service.ts
index b38525c..a45f7d1 100644
--- a/metron-interface/metron-config/src/app/service/storm.service.ts
+++ b/metron-interface/metron-config/src/app/service/storm.service.ts
@@ -20,18 +20,17 @@ import { HttpClient } from '@angular/common/http';
 import { HttpUtil } from '../util/httpUtil';
 import { TopologyStatus } from '../model/topology-status';
 import { TopologyResponse } from '../model/topology-response';
-import { APP_CONFIG } from '../app.config';
-import { IAppConfig } from '../app.config.interface';
 import { Observable, interval } from 'rxjs';
 import { map, catchError, switchMap, onErrorResumeNext } from 'rxjs/operators';
+import {AppConfigService} from './app-config.service';
 
 @Injectable()
 export class StormService {
-  url = this.config.apiEndpoint + '/storm';
+  url = this.appConfigService.getApiRoot() + '/storm';
 
   constructor(
     private http: HttpClient,
-    @Inject(APP_CONFIG) private config: IAppConfig
+    private appConfigService: AppConfigService
   ) {}
 
   public pollGetAll(): Observable<TopologyStatus[]> {
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
index cf1c495..8422544 100644
--- a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
@@ -47,7 +47,7 @@ export class AceEditorComponent implements AfterViewInit, ControlValueAccessor {
   private onChangeCallback;
 
   constructor() {
-    ace.config.set('basePath', '/assets/ace');
+    ace.config.set('basePath', 'assets/ace');
   }
 
   ngAfterViewInit() {
diff --git a/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts b/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts
index 78c246a..bffb4a3 100644
--- a/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts
+++ b/metron-interface/metron-config/src/app/shared/auth-guard.spec.ts
@@ -20,11 +20,14 @@ import {EventEmitter}     from '@angular/core';
 import {AuthGuard} from './auth-guard';
 import {AuthenticationService} from '../service/authentication.service';
 import {Router} from '@angular/router';
+import {HttpUtil} from "../util/httpUtil";
+import {HttpClientTestingModule} from "@angular/common/http/testing";
+import {AppConfigService} from '../service/app-config.service';
+import {MockAppConfigService} from '../service/mock.app-config.service';
 
-class MockAuthenticationService {
+class MockAuthenticationService extends AuthenticationService{
   _isAuthenticationChecked: boolean;
   _isAuthenticated: boolean;
-  onLoginEvent: EventEmitter<boolean> = new EventEmitter<boolean>();
 
   public isAuthenticationChecked(): boolean {
     return this._isAuthenticationChecked;
@@ -43,10 +46,12 @@ describe('AuthGuard', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
       providers: [
         AuthGuard,
         {provide: AuthenticationService, useClass: MockAuthenticationService},
-        {provide: Router, useClass: MockRouter}
+        {provide: Router, useClass: MockRouter},
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     })
       .compileComponents();
@@ -74,19 +79,18 @@ describe('AuthGuard', () => {
                                                         authenticationService: MockAuthenticationService,
                                                         router: MockRouter) => {
       authenticationService._isAuthenticationChecked = false;
+      authenticationService.onLoginEvent.next(true);
       authGuard.canActivate(null, null).subscribe(isUserValid => {
         expect(isUserValid).toBe(true);
       });
-      authenticationService.onLoginEvent.emit(true);
 
-
-      spyOn(router, 'navigateByUrl');
+      spyOn(HttpUtil, 'navigateToLogin');
       authenticationService._isAuthenticationChecked = false;
+      authenticationService.onLoginEvent.next(false);
       authGuard.canActivate(null, null).subscribe(isUserValid => {
         expect(isUserValid).toBe(false);
       });
-      authenticationService.onLoginEvent.emit(false);
-      expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+      expect(HttpUtil.navigateToLogin).toHaveBeenCalledWith();
 
     }));
 });
diff --git a/metron-interface/metron-config/src/app/shared/auth-guard.ts b/metron-interface/metron-config/src/app/shared/auth-guard.ts
index 3271853..0d74d89 100644
--- a/metron-interface/metron-config/src/app/shared/auth-guard.ts
+++ b/metron-interface/metron-config/src/app/shared/auth-guard.ts
@@ -24,6 +24,7 @@ import {
 } from '@angular/router';
 import { Observable } from 'rxjs';
 import { AuthenticationService } from '../service/authentication.service';
+import {HttpUtil} from "../util/httpUtil";
 
 @Injectable()
 export class AuthGuard implements CanActivate {
@@ -40,7 +41,7 @@ export class AuthGuard implements CanActivate {
           } else {
             observer.next(false);
             observer.complete();
-            this.router.navigateByUrl('/login');
+            HttpUtil.navigateToLogin();
           }
         });
       });
diff --git a/metron-interface/metron-config/src/app/shared/login-guard.spec.ts b/metron-interface/metron-config/src/app/shared/login-guard.spec.ts
index a49b124..8b32d0c 100644
--- a/metron-interface/metron-config/src/app/shared/login-guard.spec.ts
+++ b/metron-interface/metron-config/src/app/shared/login-guard.spec.ts
@@ -19,8 +19,11 @@ import {async, inject, TestBed} from '@angular/core/testing';
 import {AuthenticationService} from '../service/authentication.service';
 import {Router} from '@angular/router';
 import {LoginGuard} from './login-guard';
+import {HttpClientTestingModule} from "@angular/common/http/testing";
+import {AppConfigService} from '../service/app-config.service';
+import {MockAppConfigService} from '../service/mock.app-config.service';
 
-class MockAuthenticationService {
+class MockAuthenticationService extends AuthenticationService {
   public logout(): void {}
 }
 
@@ -32,10 +35,12 @@ describe('LoginGuard', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
       providers: [
         LoginGuard,
         {provide: AuthenticationService, useClass: MockAuthenticationService},
-        {provide: Router, useClass: MockRouter}
+        {provide: Router, useClass: MockRouter},
+        { provide: AppConfigService, useClass: MockAppConfigService }
       ]
     })
       .compileComponents();
@@ -50,11 +55,11 @@ describe('LoginGuard', () => {
   it('test when login is checked',
     inject([LoginGuard, AuthenticationService], (loginGuard: LoginGuard, authenticationService: MockAuthenticationService) => {
 
-      spyOn(authenticationService, 'logout');
+      spyOn(authenticationService, 'clearAuthentication');
 
       expect(loginGuard.canActivate(null, null)).toBe(true);
 
-      expect(authenticationService.logout).toHaveBeenCalled();
+      expect(authenticationService.clearAuthentication).toHaveBeenCalled();
 
   }));
 
diff --git a/metron-interface/metron-config/src/app/shared/login-guard.ts b/metron-interface/metron-config/src/app/shared/login-guard.ts
index d8b8f0d..414f590 100644
--- a/metron-interface/metron-config/src/app/shared/login-guard.ts
+++ b/metron-interface/metron-config/src/app/shared/login-guard.ts
@@ -32,7 +32,7 @@ export class LoginGuard implements CanActivate {
 
   canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
 
-    this.authService.logout();
+    this.authService.clearAuthentication();
 
     return true;
 
diff --git a/metron-interface/metron-config/src/app/util/httpUtil.ts b/metron-interface/metron-config/src/app/util/httpUtil.ts
index d8a21a5..6bc036b 100644
--- a/metron-interface/metron-config/src/app/util/httpUtil.ts
+++ b/metron-interface/metron-config/src/app/util/httpUtil.ts
@@ -19,6 +19,7 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
 import { throwError, Observable } from 'rxjs';
 
 import { RestError } from '../model/rest-error';
+import {AppConfigService} from "../service/app-config.service";
 
 export class HttpUtil {
   public static extractString(res: HttpResponse<any>): string {
@@ -36,7 +37,7 @@ export class HttpUtil {
     // We'd also dig deeper into the error to get a better message
     let restError: RestError;
     if (res.status === 401) {
-      window.location.assign('/login?sessionExpired=true');
+      HttpUtil.navigateToLogin();
     } else if (res.status !== 404) {
       restError = res;
     } else {
@@ -45,4 +46,9 @@ export class HttpUtil {
     }
     return throwError(restError);
   }
+
+  public static navigateToLogin() {
+    let loginPath = AppConfigService.getAppConfigStatic()['loginPath'];
+    location.href = loginPath;
+  }
 }
diff --git a/metron-interface/metron-config/src/assets/app-config.json b/metron-interface/metron-config/src/assets/app-config.json
new file mode 100644
index 0000000..e485071
--- /dev/null
+++ b/metron-interface/metron-config/src/assets/app-config.json
@@ -0,0 +1,4 @@
+{
+  "apiRoot": "/api/v1",
+  "loginPath": "/login"
+}
\ No newline at end of file
diff --git a/metron-interface/metron-config/src/index.html b/metron-interface/metron-config/src/index.html
index 2c3f8c5..1c88856 100644
--- a/metron-interface/metron-config/src/index.html
+++ b/metron-interface/metron-config/src/index.html
@@ -18,7 +18,8 @@
 <head>
   <meta charset="utf-8">
   <title>Metron Configuration</title>
-  <base href="/">
+    <!-- The base href is relative so that static assets are properly resolved when behind a reverse proxy -->
+  <base href="./">
 
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
diff --git a/metron-interface/metron-rest/pom.xml b/metron-interface/metron-rest/pom.xml
index 67a6820..7d54401 100644
--- a/metron-interface/metron-rest/pom.xml
+++ b/metron-interface/metron-rest/pom.xml
@@ -39,6 +39,7 @@
         <spring.version>5.0.5.RELEASE</spring.version>
         <eclipse.link.version>2.6.4</eclipse.link.version>
         <jsonpath.version>2.4.0</jsonpath.version>
+        <jwt.version>4.41.2</jwt.version>
     </properties>
     <dependencies>
       <dependency>
@@ -458,6 +459,11 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <version>${jwt.version}</version>
+        </dependency>
     </dependencies>
 
     <dependencyManagement>
diff --git a/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metron.xml b/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metron.xml
new file mode 100644
index 0000000..80550b2
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metron.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You 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.
+  -->
+<topology>
+
+  <gateway>
+    <provider>
+      <role>federation</role>
+      <name>SSOCookieProvider</name>
+      <enabled>true</enabled>
+      <param>
+        <name>sso.authentication.provider.url</name>
+        <value>https://node1:8443/gateway/metronsso/api/v1/websso</value>
+      </param>
+    </provider>
+
+    <provider>
+      <role>identity-assertion</role>
+      <name>Default</name>
+      <enabled>true</enabled>
+    </provider>
+
+    <provider>
+      <role>authorization</role>
+      <name>AclsAuthz</name>
+      <enabled>true</enabled>
+    </provider>
+
+  </gateway>
+
+  <service>
+    <role>METRON-REST</role>
+    <url>http://localhost:8082</url>
+  </service>
+
+  <service>
+    <role>METRON-ALERTS</role>
+    <url>http://localhost:4201</url>
+  </service>
+
+  <service>
+    <role>METRON-MANAGEMENT</role>
+    <url>http://localhost:4200</url>
+  </service>
+
+</topology>
\ No newline at end of file
diff --git a/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metronsso.xml b/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metronsso.xml
new file mode 100644
index 0000000..34f035d
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metronsso.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You 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.
+  -->
+<topology>
+  <gateway>
+    <provider>
+      <role>webappsec</role>
+      <name>WebAppSec</name>
+      <enabled>true</enabled>
+      <param><name>xframe.options.enabled</name><value>true</value></param>
+    </provider>
+
+    <provider>
+      <role>authentication</role>
+      <name>ShiroProvider</name>
+      <enabled>true</enabled>
+      <param>
+        <name>sessionTimeout</name>
+        <value>30</value>
+      </param>
+      <param>
+        <name>redirectToUrl</name>
+        <value>/gateway/metronsso/knoxauth/login.html</value>
+      </param>
+      <param>
+        <name>restrictedCookies</name>
+        <value>rememberme,WWW-Authenticate</value>
+      </param>
+      <param>
+        <name>main.ldapRealm</name>
+        <value>org.apache.hadoop.gateway.shirorealm.KnoxLdapRealm</value>
+      </param>
+      <param>
+        <name>main.ldapContextFactory</name>
+        <value>org.apache.hadoop.gateway.shirorealm.KnoxLdapContextFactory</value>
+      </param>
+      <param>
+        <name>main.ldapRealm.contextFactory</name>
+        <value>$ldapContextFactory</value>
+      </param>
+      <param>
+        <name>main.ldapRealm.userDnTemplate</name>
+        <value>uid={0},ou=people,dc=hadoop,dc=apache,dc=org</value>
+      </param>
+      <param>
+        <name>main.ldapRealm.contextFactory.url</name>
+        <value>ldap://localhost:33389</value>
+      </param>
+      <param>
+        <name>main.ldapRealm.authenticationCachingEnabled</name>
+        <value>false</value>
+      </param>
+      <param>
+        <name>main.ldapRealm.contextFactory.authenticationMechanism</name>
+        <value>simple</value>
+      </param>
+      <param>
+        <name>urls./**</name>
+        <value>authcBasic</value>
+      </param>
+    </provider>
+
+    <provider>
+      <role>identity-assertion</role>
+      <name>Default</name>
+      <enabled>true</enabled>
+    </provider>
+  </gateway>
+
+  <application>
+    <name>knoxauth</name>
+  </application>
+
+  <service>
+    <role>KNOXSSO</role>
+    <param>
+      <name>knoxsso.cookie.secure.only</name>
+      <value>false</value>
+    </param>
+    <param>
+      <name>knoxsso.token.ttl</name>
+      <value>30000</value>
+    </param>
+    <param>
+      <name>knoxsso.redirect.whitelist.regex</name>
+      <value>^https?:\/\/(localhost|127\.0\.0\.1|0:0:0:0:0:0:0:1|::1|node1):[0-9].*$</value>
+    </param>
+  </service>
+
+</topology>
\ No newline at end of file
diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/rewrite.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/rewrite.xml
new file mode 100644
index 0000000..f4e797b
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/rewrite.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software
+	Foundation (ASF) under one or more contributor license agreements. See the
+	NOTICE file distributed with this work for additional information regarding
+	copyright ownership. The ASF licenses this file to You 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.
+  -->
+<rules>
+  <rule dir="IN" name="METRON-ALERTS/metron-alerts/inbound/root" pattern="*://*:*/**/metron-alerts/">
+    <rewrite template="{$serviceUrl[METRON-ALERTS]}/"/>
+  </rule>
+  <rule dir="IN" name="METRON-ALERTS/metron-alerts/inbound/path" pattern="*://*:*/**/metron-alerts/{**}">
+    <rewrite template="{$serviceUrl[METRON-ALERTS]}/{**}"/>
+  </rule>
+  <rule dir="IN" name="METRON-ALERTS/metron-alerts/inbound/query" pattern="*://*:*/**/metron-alerts/{**}?{**}">
+    <rewrite template="{$serviceUrl[METRON-ALERTS]}/{**}?{**}"/>
+  </rule>
+</rules>
\ No newline at end of file
diff --git a/metron-interface/metron-alerts/src/index.html b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/service.xml
similarity index 50%
copy from metron-interface/metron-alerts/src/index.html
copy to metron-interface/metron-rest/src/main/config/knox/data/services/alerts/service.xml
index e051e27..a09be99 100644
--- a/metron-interface/metron-alerts/src/index.html
+++ b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/service.xml
@@ -1,28 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   Licensed to the Apache Software
 	Foundation (ASF) under one or more contributor license agreements. See the
 	NOTICE file distributed with this work for additional information regarding
 	copyright ownership. The ASF licenses this file to You 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
+	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.
   -->
-<!doctype html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>Metron Alerts</title>
-  <base href="/">
-
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" type="image/x-icon" href="favicon.ico">
-  <link rel="stylesheet" type="text/css" href="assets/fonts/font.css">
-</head>
-<body class="">
-  <metron-alerts-root>Loading...</metron-alerts-root>
-</body>
-</html>
+<service role="METRON-ALERTS" name="metron-alerts" version="0.6.1">
+  <routes>
+    <route path="/metron-alerts/"/>
+    <route path="/metron-alerts/**"/>
+    <route path="/metron-alerts/**?**"/>
+  </routes>
+  <dispatch classname="org.apache.hadoop.gateway.dispatch.PassAllHeadersDispatch"/>
+</service>
\ No newline at end of file
diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/management/rewrite.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/management/rewrite.xml
new file mode 100644
index 0000000..be681a7
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/config/knox/data/services/management/rewrite.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software
+        Foundation (ASF) under one or more contributor license agreements. See the
+        NOTICE file distributed with this work for additional information regarding
+        copyright ownership. The ASF licenses this file to You 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.
+  -->
+<rules>
+  <rule dir="IN" name="METRON-MANAGEMENT/metron-management/inbound/root" pattern="*://*:*/**/metron-management/">
+    <rewrite template="{$serviceUrl[METRON-MANAGEMENT]}/"/>
+  </rule>
+  <rule dir="IN" name="METRON-MANAGEMENT/metron-management/inbound/path" pattern="*://*:*/**/metron-management/{**}">
+    <rewrite template="{$serviceUrl[METRON-MANAGEMENT]}/{**}"/>
+  </rule>
+  <rule dir="IN" name="METRON-MANAGEMENT/metron-management/inbound/query" pattern="*://*:*/**/metron-management/{**}?{**}">
+    <rewrite template="{$serviceUrl[METRON-MANAGEMENT]}/{**}?{**}"/>
+  </rule>
+</rules>
\ No newline at end of file
diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/management/service.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/management/service.xml
new file mode 100644
index 0000000..02ddc77
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/config/knox/data/services/management/service.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software
+        Foundation (ASF) under one or more contributor license agreements. See the
+        NOTICE file distributed with this work for additional information regarding
+        copyright ownership. The ASF licenses this file to You 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.
+  -->
+<service role="METRON-MANAGEMENT" name="metron-management" version="0.6.1">
+  <routes>
+    <route path="/metron-management/"/>
+    <route path="/metron-management/**"/>
+    <route path="/metron-management/**?**"/>
+  </routes>
+  <dispatch classname="org.apache.hadoop.gateway.dispatch.PassAllHeadersDispatch"/>
+</service>
\ No newline at end of file
diff --git a/metron-interface/metron-alerts/src/index.html b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/rewrite.xml
similarity index 50%
copy from metron-interface/metron-alerts/src/index.html
copy to metron-interface/metron-rest/src/main/config/knox/data/services/rest/rewrite.xml
index e051e27..fabed06 100644
--- a/metron-interface/metron-alerts/src/index.html
+++ b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/rewrite.xml
@@ -1,28 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   Licensed to the Apache Software
 	Foundation (ASF) under one or more contributor license agreements. See the
 	NOTICE file distributed with this work for additional information regarding
 	copyright ownership. The ASF licenses this file to You 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
+	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.
   -->
-<!doctype html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>Metron Alerts</title>
-  <base href="/">
-
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" type="image/x-icon" href="favicon.ico">
-  <link rel="stylesheet" type="text/css" href="assets/fonts/font.css">
-</head>
-<body class="">
-  <metron-alerts-root>Loading...</metron-alerts-root>
-</body>
-</html>
+<rules>
+  <rule dir="IN" name="METRON-REST/metron-rest/inbound" pattern="*://*:*/**/metron-rest/{path=**}?{**}">
+    <rewrite template="{$serviceUrl[METRON-REST]}/{path=**}?{**}"/>
+  </rule>
+</rules>
\ No newline at end of file
diff --git a/metron-interface/metron-alerts/src/index.html b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/service.xml
similarity index 50%
copy from metron-interface/metron-alerts/src/index.html
copy to metron-interface/metron-rest/src/main/config/knox/data/services/rest/service.xml
index e051e27..d9bb56b 100644
--- a/metron-interface/metron-alerts/src/index.html
+++ b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/service.xml
@@ -1,28 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   Licensed to the Apache Software
 	Foundation (ASF) under one or more contributor license agreements. See the
 	NOTICE file distributed with this work for additional information regarding
 	copyright ownership. The ASF licenses this file to You 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
+	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.
   -->
-<!doctype html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>Metron Alerts</title>
-  <base href="/">
-
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" type="image/x-icon" href="favicon.ico">
-  <link rel="stylesheet" type="text/css" href="assets/fonts/font.css">
-</head>
-<body class="">
-  <metron-alerts-root>Loading...</metron-alerts-root>
-</body>
-</html>
+<service role="METRON-REST" name="metron-rest" version="0.6.1">
+  <routes>
+    <route path="/metron-rest/**"/>
+  </routes>
+  <dispatch classname="org.apache.hadoop.gateway.dispatch.PassAllHeadersDispatch"/>
+</service>
\ No newline at end of file
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java
index 80ac2bf..b8a8306 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java
@@ -25,6 +25,7 @@ public class MetronRestConstants {
   public static final String TEST_PROFILE = "test";
   public static final String LDAP_PROFILE = "ldap";
   public static final String DOCKER_PROFILE = "docker";
+  public static final String KNOX_PROFILE = "knox";
   public static final String CSRF_ENABLE_PROFILE = "csrf-enable";
 
   public static final String GROK_TEMP_PATH_SPRING_PROPERTY = "grok.path.temp";
@@ -71,6 +72,7 @@ public class MetronRestConstants {
   public static final String META_DAO_IMPL = "meta.dao.impl";
   public static final String META_DAO_SORT = "meta.dao.sort";
 
+  public static final String SECURITY_ROLE_PREFIX = "ROLE_";
   public static final String SECURITY_ROLE_USER = "USER";
   public static final String SECURITY_ROLE_ADMIN = "ADMIN";
 
@@ -87,4 +89,10 @@ public class MetronRestConstants {
   public static final String PCAP_PDML_SCRIPT_PATH_SPRING_PROPERTY = "pcap.pdml.script.path";
   public static final String PCAP_YARN_QUEUE_SPRING_PROPERTY = "pcap.yarn.queue";
   public static final String PCAP_FINALIZER_THREADPOOL_SIZE_SPRING_PROPERTY = "pcap.finalizer.threadpool.size";
+
+  public static final String LDAP_PROVIDER_URL_SPRING_PROPERTY = "ldap.provider.url";
+  public static final String LDAP_PROVIDER_USERDN_SPRING_PROPERTY = "ldap.provider.userdn";
+  public static final String LDAP_PROVIDER_PASSWORD_SPRING_PROPERTY = "ldap.provider.password";
+
+  public static final String KNOX_ROOT_SPRING_PROPERTY = "knox.root";
 }
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java
new file mode 100644
index 0000000..d140a2f
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java
@@ -0,0 +1,278 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.metron.rest.config;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.metron.rest.security.SecurityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.support.LdapNameBuilder;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_PREFIX;
+import static org.springframework.ldap.query.LdapQueryBuilder.query;
+
+/**
+ * This class is a Servlet Filter that authenticates a Knox SSO token.  The token is stored in a cookie and is
+ * verified against a public Knox key.  The token expiration and begin time are also validated.  Upon successful validation,
+ * a Spring Authentication object is built from the user name and user groups queried from LDAP.  Currently, user groups are
+ * mapped directly to Spring roles and prepended with "ROLE_".
+ */
+public class KnoxSSOAuthenticationFilter implements Filter {
+  private static final Logger LOG = LoggerFactory.getLogger(KnoxSSOAuthenticationFilter.class);
+
+  private String userSearchBase;
+  private Path knoxKeyFile;
+  private String knoxKeyString;
+  private String knoxCookie;
+  private LdapTemplate ldapTemplate;
+
+  public KnoxSSOAuthenticationFilter(String userSearchBase,
+                                     Path knoxKeyFile,
+                                     String knoxKeyString,
+                                     String knoxCookie,
+                                     LdapTemplate ldapTemplate) {
+    this.userSearchBase = userSearchBase;
+    this.knoxKeyFile = knoxKeyFile;
+    this.knoxKeyString = knoxKeyString;
+    this.knoxCookie = knoxCookie;
+    if (ldapTemplate == null) {
+      throw new IllegalStateException("KnoxSSO requires LDAP. You must add 'ldap' to the active profiles.");
+    }
+    this.ldapTemplate = ldapTemplate;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) {
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  /**
+   * Extracts the Knox token from the configured cookie.  If basic authentication headers are present, SSO authentication
+   * is skipped.
+   * @param request ServletRequest
+   * @param response ServletResponse
+   * @param chain FilterChain
+   * @throws IOException
+   * @throws ServletException
+   */
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+          throws IOException, ServletException {
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+    // If a basic authentication header is present, use that to authenticate and skip SSO
+    String authHeader = httpRequest.getHeader("Authorization");
+    if (authHeader == null || !authHeader.startsWith("Basic")) {
+      String serializedJWT = getJWTFromCookie(httpRequest);
+      if (serializedJWT != null) {
+        SignedJWT jwtToken;
+        try {
+          jwtToken = SignedJWT.parse(serializedJWT);
+          String userName = jwtToken.getJWTClaimsSet().getSubject();
+          LOG.info("SSO login user : {} ", userName);
+          if (isValid(jwtToken, userName)) {
+            Authentication authentication = getAuthentication(userName, httpRequest);
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+          }
+        } catch (ParseException e) {
+          LOG.warn("Unable to parse the JWT token", e);
+        }
+      }
+    }
+    chain.doFilter(request, response);
+  }
+
+  /**
+   * Validates a Knox token with expiration and begin times and verifies the token with a public Knox key.
+   * @param jwtToken Knox token
+   * @param userName User name associated with the token
+   * @return Whether a token is valid or not
+   * @throws ParseException JWT Token could not be parsed.
+   */
+  protected boolean isValid(SignedJWT jwtToken, String userName) throws ParseException {
+    // Verify the user name is present
+    if (userName == null || userName.isEmpty()) {
+      LOG.info("Could not find user name in SSO token");
+      return false;
+    }
+
+    Date now = new Date();
+
+    // Verify the token has not expired
+    Date expirationTime = jwtToken.getJWTClaimsSet().getExpirationTime();
+    if (expirationTime != null && now.after(expirationTime)) {
+      LOG.info("SSO token expired: {} ", userName);
+      return false;
+    }
+
+    // Verify the token is not before time
+    Date notBeforeTime = jwtToken.getJWTClaimsSet().getNotBeforeTime();
+    if (notBeforeTime != null && now.before(notBeforeTime)) {
+      LOG.info("SSO token not yet valid: {} ", userName);
+      return false;
+    }
+
+    return validateSignature(jwtToken);
+  }
+
+  /**
+   * Verify the signature of the JWT token in this method. This method depends on
+   * the public key that was established during init based upon the provisioned
+   * public key. Override this method in subclasses in order to customize the
+   * signature verification behavior.
+   *
+   * @param jwtToken The token that contains the signature to be validated.
+   * @return valid true if signature verifies successfully; false otherwise
+   */
+  protected boolean validateSignature(SignedJWT jwtToken) {
+    // Verify the token signature algorithm was as expected
+    String receivedSigAlg = jwtToken.getHeader().getAlgorithm().getName();
+
+    if (!receivedSigAlg.equals(JWSAlgorithm.RS256.getName())) {
+      return false;
+    }
+
+    // Verify the token has been properly signed
+    if (JWSObject.State.SIGNED == jwtToken.getState()) {
+      LOG.debug("SSO token is in a SIGNED state");
+      if (jwtToken.getSignature() != null) {
+        LOG.debug("SSO token signature is not null");
+        try {
+          JWSVerifier verifier = new RSASSAVerifier(SecurityUtils.parseRSAPublicKey(getKnoxKey()));
+          if (jwtToken.verify(verifier)) {
+            LOG.debug("SSO token has been successfully verified");
+            return true;
+          } else {
+            LOG.warn("SSO signature verification failed. Please check the public key.");
+          }
+        } catch (Exception e) {
+          LOG.warn("Error while validating signature", e);
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Encapsulate the acquisition of the JWT token from HTTP cookies within the
+   * request.
+   *
+   * @param req ServletRequest to get the JWT token from
+   * @return serialized JWT token
+   */
+  protected String getJWTFromCookie(HttpServletRequest req) {
+    String serializedJWT = null;
+    Cookie[] cookies = req.getCookies();
+    if (cookies != null) {
+      for (Cookie cookie : cookies) {
+        LOG.debug(String.format("Found cookie: %s [%s]", cookie.getName(), cookie.getValue()));
+        if (knoxCookie.equals(cookie.getName())) {
+          if (LOG.isDebugEnabled()) {
+            LOG.debug(knoxCookie + " cookie has been found and is being processed");
+          }
+          serializedJWT = cookie.getValue();
+          break;
+        }
+      }
+    } else {
+      if (LOG.isDebugEnabled()) {
+        LOG.debug(knoxCookie + " not found");
+      }
+    }
+    return serializedJWT;
+  }
+
+  /**
+   * A public Knox key can either be passed in directly or read from a file.
+   * @return Public Knox key
+   * @throws IOException There was a problem reading the Knox key file.
+   */
+  protected String getKnoxKey() throws IOException {
+    String knoxKey;
+    if ((this.knoxKeyString == null || this.knoxKeyString.isEmpty()) && this.knoxKeyFile != null) {
+      List<String> keyLines = Files.readAllLines(knoxKeyFile, StandardCharsets.UTF_8);
+      knoxKey = String.join("", keyLines);
+    } else {
+      knoxKey = this.knoxKeyString;
+    }
+    return knoxKey;
+  }
+
+  /**
+   * Builds the Spring Authentication object using the supplied user name and groups looked up from LDAP.  Groups are currently
+   * mapped directly to Spring roles by converting to upper case and prepending the name with "ROLE_".
+   * @param userName The username to build the Authentication object with.
+   * @param httpRequest HttpServletRequest
+   * @return Authentication object for the given user.
+   */
+  protected Authentication getAuthentication(String userName, HttpServletRequest httpRequest) {
+    String ldapName = LdapNameBuilder.newInstance().add(userSearchBase).add("uid", userName).build().toString();
+
+    // Search ldap for a user's groups and convert to a Spring role
+    List<GrantedAuthority> grantedAuths = ldapTemplate.search(query()
+            .where("objectclass")
+            .is("groupOfNames")
+            .and("member")
+            .is(ldapName), (AttributesMapper<String>) attrs -> (String) attrs.get("cn").get())
+            .stream()
+            .map(group -> String.format("%s%s", SECURITY_ROLE_PREFIX, group.toUpperCase()))
+            .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
+
+    final UserDetails principal = new User(userName, "", grantedAuths);
+    final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
+            principal, "", grantedAuths);
+    WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
+    authentication.setDetails(webDetails);
+    return authentication;
+  }
+
+}
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/LdapConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/LdapConfig.java
new file mode 100644
index 0000000..3bb4cd1
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/LdapConfig.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.metron.rest.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.core.env.Environment;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.support.LdapContextSource;
+
+import static org.apache.metron.rest.MetronRestConstants.LDAP_PROFILE;
+import static org.apache.metron.rest.MetronRestConstants.LDAP_PROVIDER_PASSWORD_SPRING_PROPERTY;
+import static org.apache.metron.rest.MetronRestConstants.LDAP_PROVIDER_URL_SPRING_PROPERTY;
+import static org.apache.metron.rest.MetronRestConstants.LDAP_PROVIDER_USERDN_SPRING_PROPERTY;
+
+@Configuration
+@Profile(LDAP_PROFILE)
+public class LdapConfig {
+
+  @Autowired
+  private Environment environment;
+
+  @Bean
+  public LdapTemplate ldapTemplate() {
+    LdapContextSource contextSource = new LdapContextSource();
+
+    contextSource.setUrl(environment.getProperty(LDAP_PROVIDER_URL_SPRING_PROPERTY));
+    contextSource.setUserDn(environment.getProperty(LDAP_PROVIDER_USERDN_SPRING_PROPERTY));
+    contextSource.setPassword(environment.getProperty(LDAP_PROVIDER_PASSWORD_SPRING_PROPERTY));
+    contextSource.afterPropertiesSet();
+
+    return new LdapTemplate(contextSource);
+  }
+
+}
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java
index 564ff32..d0da8cc 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java
@@ -17,22 +17,64 @@
  */
 package org.apache.metron.rest.config;
 
+import org.apache.metron.rest.MetronRestConstants;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
 import org.springframework.web.bind.annotation.RestController;
 import springfox.documentation.builders.PathSelectors;
 import springfox.documentation.builders.RequestHandlerSelectors;
 import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.paths.RelativePathProvider;
 import springfox.documentation.spring.web.plugins.Docket;
 import springfox.documentation.swagger2.annotations.EnableSwagger2;
 
+import javax.servlet.ServletContext;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.metron.rest.MetronRestConstants.KNOX_PROFILE;
+
 @Configuration
 @EnableSwagger2
 public class SwaggerConfig {
+
+  @Autowired
+  private Environment environment;
+
+  @Autowired
+  ServletContext servletContext;
+
   @Bean
   public Docket api() {
-    return new Docket(DocumentationType.SWAGGER_2)
-            .select()
+    List<String> activeProfiles = Arrays.asList(environment.getActiveProfiles());
+    Docket docket = new Docket(DocumentationType.SWAGGER_2);
+    if (activeProfiles.contains(KNOX_PROFILE)) {
+      String knoxRoot = environment.getProperty(MetronRestConstants.KNOX_ROOT_SPRING_PROPERTY, String.class, "");
+      docket = docket.pathProvider(new RelativePathProvider (servletContext) {
+        @Override
+        protected String applicationPath() {
+          return knoxRoot;
+        }
+
+        @Override
+        protected String getDocumentationPath() {
+          return knoxRoot;
+        }
+
+        @Override
+        public String getApplicationBasePath() {
+          return knoxRoot;
+        }
+
+        @Override
+        public String getOperationPath(String operationPath) {
+          return knoxRoot + super.getOperationPath(operationPath);
+        }
+      });
+    }
+    return docket.select()
             .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
             .paths(PathSelectors.any())
             .build();
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java
index 7ca3a46..835889c 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java
@@ -20,8 +20,16 @@ package org.apache.metron.rest.config;
 import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_ADMIN;
 import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_USER;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.interfaces.RSAPublicKey;
 import java.util.Arrays;
 import java.util.List;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import javax.sql.DataSource;
 import org.apache.metron.rest.MetronRestConstants;
 import org.slf4j.Logger;
@@ -31,16 +39,32 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.env.Environment;
+import org.springframework.http.HttpMethod;
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.security.authentication.ProviderManager;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
 import org.springframework.security.crypto.password.NoOpPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
 import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
@@ -58,27 +82,49 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     @Autowired
     private DataSource dataSource;
 
+    @Autowired(required = false)
+    private LdapTemplate ldapTemplate;
+
     @Value("${ldap.provider.url}")
     private String providerUrl;
+
     @Value("${ldap.provider.userdn}")
     private String providerUserDn;
+
     @Value("${ldap.provider.password}")
     private String providerPassword;
+
     @Value("${ldap.user.dn.patterns}")
     private String userDnPatterns;
+
     @Value("${ldap.user.passwordAttribute}")
     private String passwordAttribute;
+
     @Value("${ldap.user.searchBase}")
     private String userSearchBase;
+
     @Value("${ldap.user.searchFilter}")
     private String userSearchFilter;
+
     @Value("${ldap.group.searchBase}")
     private String groupSearchBase;
+
     @Value("${ldap.group.roleAttribute}")
     private String groupRoleAttribute;
+
     @Value("${ldap.group.searchFilter}")
     private String groupSearchFilter;
 
+    @Value("${knox.sso.pubkeyFile:}")
+    private Path knoxKeyFile;
+
+    @Value("${knox.sso.pubkey:}")
+    private String knoxKeyString;
+
+    @Value("${knox.sso.cookie:hadoop-jwt}")
+    private String knoxCookie;
+
+
     @RequestMapping(value = {"/login", "/logout", "/sensors", "/sensors*/**"}, method = RequestMethod.GET)
     public String handleNGRequests() {
         return "forward:/index.html";
@@ -100,14 +146,21 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .and().httpBasic()
                 .and()
                 .logout()
+                .logoutUrl("/api/v1/logout")
                 .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                 .invalidateHttpSession(true)
-                .deleteCookies("JSESSIONID");
-        if (Arrays.asList(environment.getActiveProfiles()).contains(MetronRestConstants.CSRF_ENABLE_PROFILE)) {
+                .deleteCookies("JSESSIONID", knoxCookie);
+
+        List<String> activeProfiles = Arrays.asList(environment.getActiveProfiles());
+        if (activeProfiles.contains(MetronRestConstants.CSRF_ENABLE_PROFILE)) {
             http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
         } else {
             http.csrf().disable();
         }
+        if (activeProfiles.contains(MetronRestConstants.KNOX_PROFILE)) {
+          http.addFilterAt(new KnoxSSOAuthenticationFilter(userSearchBase, knoxKeyFile, knoxKeyString,
+                  knoxCookie, ldapTemplate), UsernamePasswordAuthenticationFilter.class);
+        }
     }
 
     @Autowired
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java
index fe2968f..6125017 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java
@@ -18,6 +18,7 @@
 package org.apache.metron.rest.controller;
 
 import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_ADMIN;
+import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_PREFIX;
 
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -74,7 +75,7 @@ public class AlertsUIController {
     }
   }
 
-  @Secured({"ROLE_" + SECURITY_ROLE_ADMIN})
+  @Secured({SECURITY_ROLE_PREFIX + SECURITY_ROLE_ADMIN})
   @ApiOperation(value = "Retrieves all users' settings.  Only users that are part of "
           + "the \"ROLE_ADMIN\" role are allowed to get all user settings.")
   @ApiResponses(value = {@ApiResponse(message = "List of all user settings", code = 200),
@@ -103,7 +104,7 @@ public class AlertsUIController {
     return responseEntity;
   }
 
-  @Secured({"ROLE_" + SECURITY_ROLE_ADMIN})
+  @Secured({SECURITY_ROLE_PREFIX + SECURITY_ROLE_ADMIN})
   @ApiOperation(value = "Deletes a user's settings.  Only users that are part of "
           + "the \"ROLE_ADMIN\" role are allowed to delete user settings.")
   @ApiResponses(value = {@ApiResponse(message = "User settings were deleted", code = 200),
diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java
index ed10f14..5da1a06 100644
--- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java
+++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java
@@ -21,6 +21,15 @@ package org.apache.metron.rest.security;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetails;
 
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+
 public class SecurityUtils {
 
   /** Returns the username of the currently logged in user.
@@ -37,4 +46,28 @@ public class SecurityUtils {
     }
     return user;
   }
+
+  public static RSAPublicKey parseRSAPublicKey(String pem)
+          throws CertificateException, UnsupportedEncodingException {
+    String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n";
+    String PEM_FOOTER = "\n-----END CERTIFICATE-----";
+    String fullPem = (pem.startsWith(PEM_HEADER) && pem.endsWith(PEM_FOOTER)) ? pem : PEM_HEADER + pem + PEM_FOOTER;
+    PublicKey key = null;
+    try {
+      CertificateFactory fact = CertificateFactory.getInstance("X.509");
+      ByteArrayInputStream is = new ByteArrayInputStream(fullPem.getBytes(StandardCharsets.UTF_8));
+      X509Certificate cer = (X509Certificate) fact.generateCertificate(is);
+      key = cer.getPublicKey();
+    } catch (CertificateException ce) {
+      String message = null;
+      if (pem.startsWith(PEM_HEADER)) {
+        message = "CertificateException - be sure not to include PEM header "
+                + "and footer in the PEM configuration element.";
+      } else {
+        message = "CertificateException - PEM may be corrupt";
+      }
+      throw new CertificateException(message, ce);
+    }
+    return (RSAPublicKey) key;
+  }
 }
diff --git a/metron-interface/metron-rest/src/main/scripts/install_metron_knox.sh b/metron-interface/metron-rest/src/main/scripts/install_metron_knox.sh
new file mode 100755
index 0000000..7e87393
--- /dev/null
+++ b/metron-interface/metron-rest/src/main/scripts/install_metron_knox.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+METRON_VERSION=${project.version}
+METRON_HOME=${METRON_HOME:-/usr/metron/${METRON_VERSION}}
+KNOX_HOME=${KNOX_HOME:-/usr/hdp/current/knox-server}
+KNOX_METRON_REST_DIR=$KNOX_HOME/data/services/metron-rest/$METRON_VERSION
+KNOX_METRON_ALERTS_DIR=$KNOX_HOME/data/services/metron-alerts/$METRON_VERSION
+KNOX_METRON_MANAGEMENT_DIR=$KNOX_HOME/data/services/metron-management/$METRON_VERSION
+
+mkdir -p $KNOX_METRON_REST_DIR
+mkdir -p $KNOX_METRON_ALERTS_DIR
+mkdir -p $KNOX_METRON_MANAGEMENT_DIR
+
+cp $METRON_HOME/config/knox/data/services/rest/* $KNOX_METRON_REST_DIR
+cp $METRON_HOME/config/knox/data/services/alerts/* $KNOX_METRON_ALERTS_DIR
+cp $METRON_HOME/config/knox/data/services/management/* $KNOX_METRON_MANAGEMENT_DIR
+cp $METRON_HOME/config/knox/conf/topologies/metron.xml $KNOX_HOME/conf/topologies
+cp $METRON_HOME/config/knox/conf/topologies/metronsso.xml $KNOX_HOME/conf/topologies
diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilterTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilterTest.java
new file mode 100644
index 0000000..e00bc70
--- /dev/null
+++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilterTest.java
@@ -0,0 +1,388 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.metron.rest.config;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.commons.io.FileUtils;
+import org.apache.metron.rest.security.SecurityUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.query.LdapQuery;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Date;
+
+import static junit.framework.TestCase.assertNull;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+import static org.powermock.api.mockito.PowerMockito.whenNew;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({KnoxSSOAuthenticationFilter.class, SignedJWT.class, SecurityContextHolder.class, SecurityUtils.class, RSASSAVerifier.class})
+public class KnoxSSOAuthenticationFilterTest {
+
+  @Rule
+  public final ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void shouldThrowExceptionOnMissingLdapTemplate() {
+    exception.expect(IllegalStateException.class);
+    exception.expectMessage("KnoxSSO requires LDAP. You must add 'ldap' to the active profiles.");
+
+    new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            null
+            );
+  }
+
+  @Test
+  public void doFilterShouldProperlySetAuthentication() throws Exception {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    ServletResponse response = mock(ServletResponse.class);
+    FilterChain chain = mock(FilterChain.class);
+    SignedJWT signedJWT = mock(SignedJWT.class);
+    mockStatic(SignedJWT.class);
+    JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("userName").build();
+    Authentication authentication = mock(Authentication.class);
+    SecurityContext securityContext = mock(SecurityContext.class);
+    mockStatic(SecurityContextHolder.class);
+
+    when(request.getHeader("Authorization")).thenReturn(null);
+    doReturn("serializedJWT").when(knoxSSOAuthenticationFilter).getJWTFromCookie(request);
+    when(SignedJWT.parse("serializedJWT")).thenReturn(signedJWT);
+    when(signedJWT.getJWTClaimsSet()).thenReturn(jwtClaimsSet);
+    doReturn(true).when(knoxSSOAuthenticationFilter).isValid(signedJWT, "userName");
+    doReturn(authentication).when(knoxSSOAuthenticationFilter).getAuthentication("userName", request);
+    when(SecurityContextHolder.getContext()).thenReturn(securityContext);
+
+    knoxSSOAuthenticationFilter.doFilter(request, response, chain);
+
+    verify(securityContext).setAuthentication(authentication);
+    verify(chain).doFilter(request, response);
+    verifyNoMoreInteractions(chain, securityContext);
+  }
+
+  @Test
+  public void doFilterShouldContinueOnBasicAuthenticationHeader() throws Exception {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    ServletResponse response = mock(ServletResponse.class);
+    FilterChain chain = mock(FilterChain.class);
+
+    when(request.getHeader("Authorization")).thenReturn("Basic ");
+
+    knoxSSOAuthenticationFilter.doFilter(request, response, chain);
+
+    verify(knoxSSOAuthenticationFilter, times(0)).getJWTFromCookie(request);
+    verify(chain).doFilter(request, response);
+    verifyNoMoreInteractions(chain);
+  }
+
+  @Test
+  public void doFilterShouldContinueOnParseException() throws Exception {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    ServletResponse response = mock(ServletResponse.class);
+    FilterChain chain = mock(FilterChain.class);
+    SignedJWT signedJWT = mock(SignedJWT.class);
+    mockStatic(SignedJWT.class);
+
+    when(request.getHeader("Authorization")).thenReturn(null);
+    doReturn("serializedJWT").when(knoxSSOAuthenticationFilter).getJWTFromCookie(request);
+    when(SignedJWT.parse("serializedJWT")).thenThrow(new ParseException("parse exception", 0));
+
+    knoxSSOAuthenticationFilter.doFilter(request, response, chain);
+
+    verify(knoxSSOAuthenticationFilter, times(0)).getAuthentication("userName", request);
+    verify(chain).doFilter(request, response);
+    verifyNoMoreInteractions(chain);
+  }
+
+  @Test
+  public void doFilterShouldContinueOnInvalidToken() throws Exception {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    ServletResponse response = mock(ServletResponse.class);
+    FilterChain chain = mock(FilterChain.class);
+    SignedJWT signedJWT = mock(SignedJWT.class);
+    mockStatic(SignedJWT.class);
+    JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("userName").build();
+
+    when(request.getHeader("Authorization")).thenReturn(null);
+    doReturn("serializedJWT").when(knoxSSOAuthenticationFilter).getJWTFromCookie(request);
+    when(SignedJWT.parse("serializedJWT")).thenReturn(signedJWT);
+    when(signedJWT.getJWTClaimsSet()).thenReturn(jwtClaimsSet);
+    doReturn(false).when(knoxSSOAuthenticationFilter).isValid(signedJWT, "userName");
+
+    knoxSSOAuthenticationFilter.doFilter(request, response, chain);
+
+    verify(knoxSSOAuthenticationFilter, times(0)).getAuthentication("userName", request);
+    verify(chain).doFilter(request, response);
+    verifyNoMoreInteractions(chain);
+  }
+
+  @Test
+  public void isValidShouldProperlyValidateToken() throws Exception {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+
+    SignedJWT jwtToken = mock(SignedJWT.class);
+
+    {
+      // Should be invalid on emtpy user name
+      assertFalse(knoxSSOAuthenticationFilter.isValid(jwtToken, null));
+    }
+
+    {
+      // Should be invalid on expired token
+      Date expiredDate = new Date(System.currentTimeMillis() - 10000);
+      JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().expirationTime(expiredDate).build();
+      when(jwtToken.getJWTClaimsSet()).thenReturn(jwtClaimsSet);
+
+      assertFalse(knoxSSOAuthenticationFilter.isValid(jwtToken, "userName"));
+    }
+
+    {
+      // Should be invalid when date is before notBeforeTime
+      Date notBeforeDate = new Date(System.currentTimeMillis() + 10000);
+      JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().notBeforeTime(notBeforeDate).build();
+      when(jwtToken.getJWTClaimsSet()).thenReturn(jwtClaimsSet);
+
+      assertFalse(knoxSSOAuthenticationFilter.isValid(jwtToken, "userName"));
+    }
+
+    {
+      // Should be valid if user name is present and token is within time constraints
+      Date expiredDate = new Date(System.currentTimeMillis() + 10000);
+      Date notBeforeDate = new Date(System.currentTimeMillis() - 10000);
+      JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().expirationTime(expiredDate).notBeforeTime(notBeforeDate).build();
+      when(jwtToken.getJWTClaimsSet()).thenReturn(jwtClaimsSet);
+      doReturn(true).when(knoxSSOAuthenticationFilter).validateSignature(jwtToken);
+
+      assertTrue(knoxSSOAuthenticationFilter.isValid(jwtToken, "userName"));
+    }
+
+  }
+
+  @Test
+  public void validateSignatureShouldProperlyValidateToken() throws Exception {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+
+    SignedJWT jwtToken = mock(SignedJWT.class);
+
+    {
+      // Should be invalid if algorithm is not ES256
+      JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.ES384);
+      when(jwtToken.getHeader()).thenReturn(jwsHeader);
+
+      assertFalse(knoxSSOAuthenticationFilter.validateSignature(jwtToken));
+    }
+    {
+      // Should be invalid if state is not SIGNED
+      JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.RS256);
+      when(jwtToken.getHeader()).thenReturn(jwsHeader);
+      when(jwtToken.getState()).thenReturn(JWSObject.State.UNSIGNED);
+
+      assertFalse(knoxSSOAuthenticationFilter.validateSignature(jwtToken));
+    }
+    {
+      // Should be invalid if signature is null
+      JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.RS256);
+      when(jwtToken.getHeader()).thenReturn(jwsHeader);
+      when(jwtToken.getState()).thenReturn(JWSObject.State.SIGNED);
+
+      assertFalse(knoxSSOAuthenticationFilter.validateSignature(jwtToken));
+    }
+    {
+      Base64URL signature = mock(Base64URL.class);
+      when(jwtToken.getSignature()).thenReturn(signature);
+      RSAPublicKey rsaPublicKey = mock(RSAPublicKey.class);
+      RSASSAVerifier rsaSSAVerifier = mock(RSASSAVerifier.class);
+      mockStatic(SecurityUtils.class);
+      when(SecurityUtils.parseRSAPublicKey("knoxKeyString")).thenReturn(rsaPublicKey);
+      whenNew(RSASSAVerifier.class).withArguments(rsaPublicKey).thenReturn(rsaSSAVerifier);
+      {
+        // Should be invalid if token verify throws an exception
+        when(jwtToken.verify(rsaSSAVerifier)).thenThrow(new JOSEException("verify exception"));
+
+        assertFalse(knoxSSOAuthenticationFilter.validateSignature(jwtToken));
+      }
+      {
+        // Should be invalid if RSA verification fails
+        doReturn(false).when(jwtToken).verify(rsaSSAVerifier);
+
+        assertFalse(knoxSSOAuthenticationFilter.validateSignature(jwtToken));
+      }
+      {
+        // Should be valid if RSA verification succeeds
+        doReturn(true).when(jwtToken).verify(rsaSSAVerifier);
+
+        assertTrue(knoxSSOAuthenticationFilter.validateSignature(jwtToken));
+      }
+    }
+  }
+
+  @Test
+  public void getJWTFromCookieShouldProperlyReturnToken() {
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            mock(LdapTemplate.class)
+    ));
+
+    HttpServletRequest request = mock(HttpServletRequest.class);
+
+    {
+      // Should be null if cookies are empty
+      assertNull(knoxSSOAuthenticationFilter.getJWTFromCookie(request));
+    }
+    {
+      // Should be null if Knox cookie is missing
+      Cookie cookie = new Cookie("someCookie", "someValue");
+      when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+      assertNull(knoxSSOAuthenticationFilter.getJWTFromCookie(request));
+    }
+    {
+      // Should return token from knoxCookie
+      Cookie cookie = new Cookie("knoxCookie", "token");
+      when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+      assertEquals("token", knoxSSOAuthenticationFilter.getJWTFromCookie(request));
+    }
+
+  }
+
+  @Test
+  public void getKnoxKeyShouldProperlyReturnKnoxKey() throws Exception {
+    {
+      KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+              mock(Path.class),
+              "knoxKeyString",
+              "knoxCookie",
+              mock(LdapTemplate.class)
+      ));
+
+      assertEquals("knoxKeyString", knoxSSOAuthenticationFilter.getKnoxKey());
+    }
+    {
+
+      FileUtils.writeStringToFile(new File("./target/knoxKeyFile"), "knoxKeyFileKeyString", StandardCharsets.UTF_8);
+      KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("userSearchBase",
+              Paths.get("./target/knoxKeyFile"),
+              null,
+              "knoxCookie",
+              mock(LdapTemplate.class)
+      ));
+
+      assertEquals("knoxKeyFileKeyString", knoxSSOAuthenticationFilter.getKnoxKey());
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Test
+  public void getAuthenticationShouldProperlyPopulateAuthentication() throws Exception {
+    LdapTemplate ldapTemplate = mock(LdapTemplate.class);
+    KnoxSSOAuthenticationFilter knoxSSOAuthenticationFilter = spy(new KnoxSSOAuthenticationFilter("ou=people,dc=hadoop,dc=apache,dc=org",
+            mock(Path.class),
+            "knoxKeyString",
+            "knoxCookie",
+            ldapTemplate
+    ));
+
+    HttpServletRequest request = mock(HttpServletRequest.class);
+
+    when(ldapTemplate.search(any(LdapQuery.class), any(AttributesMapper.class))).thenReturn(Arrays.asList("USER", "ADMIN"));
+
+    Authentication authentication = knoxSSOAuthenticationFilter.getAuthentication("userName", request);
+    Object[] grantedAuthorities = authentication.getAuthorities().toArray();
+    assertEquals("ROLE_USER", grantedAuthorities[0].toString());
+    assertEquals("ROLE_ADMIN", grantedAuthorities[1].toString());
+    assertEquals("userName", authentication.getName());
+  }
+
+}


Mime
View raw message