You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ja...@apache.org on 2018/12/12 09:53:00 UTC

lucene-solr:master: SOLR-7896: Add a login page to Admin UI, with initial support for Basic Auth

Repository: lucene-solr
Updated Branches:
  refs/heads/master 5affe7421 -> 280f67927


SOLR-7896: Add a login page to Admin UI, with initial support for Basic Auth


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/280f6792
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/280f6792
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/280f6792

Branch: refs/heads/master
Commit: 280f67927e7590c40b1d5f2960b9c6c7d21d6b5c
Parents: 5affe74
Author: Jan Høydahl <ja...@apache.org>
Authored: Wed Dec 12 10:35:15 2018 +0100
Committer: Jan Høydahl <ja...@apache.org>
Committed: Wed Dec 12 10:37:23 2018 +0100

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   2 +
 solr/NOTICE.txt                                 |   6 +
 .../apache/solr/security/BasicAuthPlugin.java   |  45 +++-
 .../security/Sha256AuthenticationProvider.java  |   7 +-
 .../apache/solr/servlet/SolrDispatchFilter.java |  40 ++--
 ...uthentication-and-authorization-plugins.adoc |  10 +
 .../src/basic-authentication-plugin.adoc        |  45 +++-
 solr/webapp/web/WEB-INF/web.xml                 |   2 +-
 solr/webapp/web/css/angular/login.css           | 103 +++++++++
 solr/webapp/web/css/angular/menu.css            |   2 +
 solr/webapp/web/index.html                      | 160 +++++++-------
 solr/webapp/web/js/angular/app.js               |  45 +++-
 solr/webapp/web/js/angular/controllers/login.js | 146 +++++++++++++
 solr/webapp/web/js/angular/services.js          |  24 +-
 solr/webapp/web/libs/angular-utf8-base64.js     | 217 +++++++++++++++++++
 solr/webapp/web/libs/angular-utf8-base64.min.js |  45 ++++
 solr/webapp/web/partials/login.html             |  80 +++++++
 17 files changed, 860 insertions(+), 119 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 9ab80f4..bfa3666 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -155,6 +155,8 @@ New Features
 * SOLR-12839: JSON 'terms' Faceting now supports a 'prelim_sort' option to use when initially selecting 
   the top ranking buckets, prior to the final 'sort' option used after refinement.  (hossman)
 
+* SOLR-7896: Add a login page to Admin UI, with initial support for Basic Auth (janhoy)
+
 Bug Fixes
 ----------------------
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/NOTICE.txt
----------------------------------------------------------------------
diff --git a/solr/NOTICE.txt b/solr/NOTICE.txt
index ad72eab..9e9a93f 100644
--- a/solr/NOTICE.txt
+++ b/solr/NOTICE.txt
@@ -46,6 +46,12 @@ Copyright (c) 2008-2014, Ryan McGeary, https://github.com/rmm5t/jquery-timeago
 This product includes require.js Javascript library created by James Burke
 Copyright (C) 2010-2014 James Burke, https://github.com/jrburke/requirejs
 
+This product includes angular-utf8-base64.js Javascript library created by Andrey Bezyazychniy
+Copyright (c) 2014 Andrey Bezyazychniy, https://github.com/stranger82/angular-utf8-base64
+
+This product includes code copied and modified from the www-authenticate Javascript library 
+Copyright (c) 2013 Randy McLaughlin, MIT-license, https://github.com/randymized/www-authenticate
+
 This product includes fugue icons created by Yusuke Kamiyamane
 Copyright (C) 2013-2014 Yusuke Kamiyamane, https://github.com/yusukekamiyamane/fugue-icons
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
index eab89e3..1212452 100644
--- a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
@@ -34,12 +34,13 @@ import java.util.StringTokenizer;
 import com.google.common.collect.ImmutableSet;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
 import org.apache.http.auth.BasicUserPrincipal;
 import org.apache.http.message.BasicHeader;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.util.ValidatingJsonMap;
-import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.SpecProvider;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.ValidatingJsonMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -47,6 +48,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   private AuthenticationProvider authenticationProvider;
   private final static ThreadLocal<Header> authHeader = new ThreadLocal<>();
+  private static final String X_REQUESTED_WITH_HEADER = "X-Requested-With";
   private boolean blockUnknown = false;
 
   public boolean authenticate(String username, String pwd) {
@@ -55,7 +57,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
 
   @Override
   public void init(Map<String, Object> pluginConfig) {
-    Object o = pluginConfig.get(BLOCK_UNKNOWN);
+    Object o = pluginConfig.get(PROPERTY_BLOCK_UNKNOWN);
     if (o != null) {
       try {
         blockUnknown = Boolean.parseBoolean(o.toString());
@@ -94,9 +96,18 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
     return provider;
   }
 
-  private void authenticationFailure(HttpServletResponse response, String message) throws IOException {
+  private void authenticationFailure(HttpServletResponse response, boolean isAjaxRequest, String message) throws IOException {
     for (Map.Entry<String, String> entry : authenticationProvider.getPromptHeaders().entrySet()) {
-      response.setHeader(entry.getKey(), entry.getValue());
+      String value = entry.getValue();
+      // Prevent browser from intercepting basic authentication header when reqeust from Admin UI
+      if (isAjaxRequest && HttpHeaders.WWW_AUTHENTICATE.equalsIgnoreCase(entry.getKey()) && value != null) {
+        if (value.startsWith("Basic ")) {
+          value = "x" + value;
+          log.debug("Prefixing {} header for Basic Auth with 'x' to prevent browser basic auth popup", 
+              HttpHeaders.WWW_AUTHENTICATE);
+        }
+      }
+      response.setHeader(entry.getKey(), value);
     }
     response.sendError(401, message);
   }
@@ -108,6 +119,8 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
     HttpServletResponse response = (HttpServletResponse) servletResponse;
 
     String authHeader = request.getHeader("Authorization");
+    boolean isAjaxRequest = isAjaxRequest(request);
+    
     if (authHeader != null) {
       BasicAuthPlugin.authHeader.set(new BasicHeader("Authorization", authHeader));
       StringTokenizer st = new StringTokenizer(authHeader);
@@ -122,7 +135,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
               String pwd = credentials.substring(p + 1).trim();
               if (!authenticate(username, pwd)) {
                 log.debug("Bad auth credentials supplied in Authorization header");
-                authenticationFailure(response, "Bad credentials");
+                authenticationFailure(response, isAjaxRequest, "Bad credentials");
               } else {
                 HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
                   @Override
@@ -135,7 +148,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
               }
 
             } else {
-              authenticationFailure(response, "Invalid authentication token");
+              authenticationFailure(response, isAjaxRequest, "Invalid authentication token");
             }
           } catch (UnsupportedEncodingException e) {
             throw new Error("Couldn't retrieve authentication", e);
@@ -144,7 +157,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
       }
     } else {
       if (blockUnknown) {
-        authenticationFailure(response, "require authentication");
+        authenticationFailure(response, isAjaxRequest, "require authentication");
       } else {
         request.setAttribute(AuthenticationPlugin.class.getName(), authenticationProvider.getPromptHeaders());
         filterChain.doFilter(request, response);
@@ -180,8 +193,16 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
     return blockUnknown;
   }
 
-  public static final String BLOCK_UNKNOWN = "blockUnknown";
-  private static final Set<String> PROPS = ImmutableSet.of(BLOCK_UNKNOWN);
-
-
+  public static final String PROPERTY_BLOCK_UNKNOWN = "blockUnknown";
+  public static final String PROPERTY_REALM = "realm";
+  private static final Set<String> PROPS = ImmutableSet.of(PROPERTY_BLOCK_UNKNOWN, PROPERTY_REALM);
+
+  /**
+   * Check if the request is an AJAX request, i.e. from the Admin UI or other SPA front 
+   * @param request the servlet request
+   * @return true if the request is AJAX request
+   */
+  private boolean isAjaxRequest(HttpServletRequest request) {
+    return "XMLHttpRequest".equalsIgnoreCase(request.getHeader(X_REQUESTED_WITH_HEADER));
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
index 4b85c45..e8cc87a 100644
--- a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
+++ b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
@@ -64,8 +64,11 @@ public class Sha256AuthenticationProvider implements ConfigEditablePlugin,  Basi
 
   @Override
   public void init(Map<String, Object> pluginConfig) {
-    if (pluginConfig.get("realm") != null) this.realm = (String) pluginConfig.get("realm");
-    else this.realm = "solr";
+    if (pluginConfig.containsKey(BasicAuthPlugin.PROPERTY_REALM)) {
+      this.realm = (String) pluginConfig.get(BasicAuthPlugin.PROPERTY_REALM);
+    } else {
+      this.realm = "solr";
+    }
     
     promptHeader = Collections.unmodifiableMap(Collections.singletonMap("WWW-Authenticate", "Basic realm=\"" + realm + "\""));
     credentials = new LinkedHashMap<>();

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
index 9e6523b..1a8b14e 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
@@ -360,18 +360,6 @@ public class SolrDispatchFilter extends BaseSolrFilter {
         }
       }
 
-      AtomicReference<HttpServletRequest> wrappedRequest = new AtomicReference<>();
-      if (!authenticateRequest(request, response, wrappedRequest)) { // the response and status code have already been sent
-        return;
-      }
-      if (wrappedRequest.get() != null) {
-        request = wrappedRequest.get();
-      }
-
-      if (cores.getAuthenticationPlugin() != null) {
-        log.debug("User principal: {}", request.getUserPrincipal());
-      }
-
       // No need to even create the HttpSolrCall object if this path is excluded.
       if (excludePatterns != null) {
         String requestPath = request.getServletPath();
@@ -389,6 +377,18 @@ public class SolrDispatchFilter extends BaseSolrFilter {
         }
       }
 
+      AtomicReference<HttpServletRequest> wrappedRequest = new AtomicReference<>();
+      if (!authenticateRequest(request, response, wrappedRequest)) { // the response and status code have already been sent
+        return;
+      }
+      if (wrappedRequest.get() != null) {
+        request = wrappedRequest.get();
+      }
+
+      if (cores.getAuthenticationPlugin() != null) {
+        log.debug("User principal: {}", request.getUserPrincipal());
+      }
+
       HttpSolrCall call = getHttpSolrCall(request, response, retry);
       ExecutorUtil.setServerThreadFlag(Boolean.TRUE);
       try {
@@ -458,8 +458,20 @@ public class SolrDispatchFilter extends BaseSolrFilter {
       // /admin/info/key must be always open. see SOLR-9188
       // tests work only w/ getPathInfo
       //otherwise it's just enough to have getServletPath()
-      if (PublicKeyHandler.PATH.equals(request.getServletPath()) ||
-          PublicKeyHandler.PATH.equals(request.getPathInfo())) return true;
+      String requestPath = request.getPathInfo();
+      if (requestPath == null) 
+        requestPath = request.getServletPath();
+      if (PublicKeyHandler.PATH.equals(requestPath)) {
+        if (log.isDebugEnabled())
+          log.debug("Pass through PKI authentication endpoint");
+        return true;
+      }
+      // /solr/ (Admin UI) must be always open to allow displaying Admin UI with login page  
+      if ("/solr/".equals(requestPath) || "/".equals(requestPath)) {
+        if (log.isDebugEnabled())
+          log.debug("Pass through Admin UI entry point");
+        return true;
+      }
       String header = request.getHeader(PKIAuthenticationPlugin.HEADER);
       if (header != null && cores.getPkiAuthenticationPlugin() != null)
         authenticationPlugin = cores.getPkiAuthenticationPlugin();

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc b/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
index 971dbcd..dabd869 100644
--- a/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
+++ b/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
@@ -157,6 +157,16 @@ Solr has one implementation of an authorization plugin:
 
 * <<rule-based-authorization-plugin.adoc#rule-based-authorization-plugin,Rule-Based Authorization Plugin>>
 
+== Authenticating in the Admin UI
+
+Whenever an authentication plugin is enabled, authentication is also required for all or some operations in the Admin UI. The Admin UI is an AngularJS application running inside your browser, and is treated as any other external client by Solr.
+
+When authentication is required the Admin UI will presented you with a login dialogue. The authentication plugins currently supported by the Admin UI are:
+
+* `BasicAuthPlugin`
+ 
+If your plugin of choice is not supported, you will have to interact with Solr sending HTTP requests instead of through the graphical user interface of the Admin UI. All operations supported by Admin UI can be performed through Solr's RESTful APIs.
+
 == Securing Inter-Node Requests
 
 There are a lot of requests that originate from the Solr nodes itself. For example, requests from overseer to nodes, recovery threads, etc. Each Authentication plugin declares whether it is capable of securing inter-node requests or not. If not, Solr will fall back to using a special internode authentication mechanism where each Solr node is a super user and is fully trusted by other Solr nodes, described below.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
index 88e8a0c..308c5a2 100644
--- a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
@@ -36,13 +36,14 @@ An example `security.json` showing both sections is shown below to show how thes
 "authentication":{ <1>
    "blockUnknown": true, <2>
    "class":"solr.BasicAuthPlugin",
-   "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="} <3>
+   "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}, <3>
+   "realm":"My Solr users" <4>
 },
 "authorization":{
    "class":"solr.RuleBasedAuthorizationPlugin",
    "permissions":[{"name":"security-edit",
-      "role":"admin"}], <4>
-   "user-role":{"solr":"admin"} <5>
+      "role":"admin"}], <5>
+   "user-role":{"solr":"admin"} <6>
 }}
 ----
 
@@ -51,13 +52,16 @@ There are several things defined in this file:
 <1> Basic authentication and rule-based authorization plugins are enabled.
 <2> The parameter `"blockUnknown":true` means that unauthenticated requests are not allowed to pass through.
 <3> A user called 'solr', with a password `'SolrRocks'` has been defined.
-<4> The 'admin' role has been defined, and it has permission to edit security settings.
-<5> The 'solr' user has been defined to the 'admin' role.
+<4> We override the `realm` property to display another text on the login prompt.
+<5> The 'admin' role has been defined, and it has permission to edit security settings.
+<6> The 'solr' user has been defined to the 'admin' role.
 
 Save your settings to a file called `security.json` locally. If you are using Solr in standalone mode, you should put this file in `$SOLR_HOME`.
 
 If `blockUnknown` does not appear in the `security.json` file, it will default to `false`. This has the effect of not requiring authentication at all. In some cases, you may want this; for example, if you want to have `security.json` in place but aren't ready to enable authentication. However, you will want to ensure that this parameter is set to `true` in order for authentication to be truly enabled in your system.
 
+If `realm` is not defined, it will default to `solr`.
+
 If you are using SolrCloud, you must upload `security.json` to ZooKeeper. You can use this example command, ensuring that the ZooKeeper port is correct:
 
 [source,bash]
@@ -139,11 +143,11 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
 
 === Set a Property
 
-Set properties for the authentication plugin. The only currently supported property for the Basic Authentication plugin is `blockUnknown`.
+Set properties for the authentication plugin. The currently supported properties for the Basic Authentication plugin are `blockUnknown` and `realm`.
 
 [.dynamic-tabs]
 --
-[example.tab-pane#v1set-property]
+[example.tab-pane#v1set-property-blockUnknown]
 ====
 [.tab-label]*V1 API*
 
@@ -153,7 +157,7 @@ curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'C
 ----
 ====
 
-[example.tab-pane#v2set-property]
+[example.tab-pane#v2set-property-blockUnknown]
 ====
 [.tab-label]*V2 API*
 
@@ -164,6 +168,31 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
 ====
 --
 
+The authentication realm defaults to `solr` and is displayed in the `WWW-Authenticate` HTTP header and in the Admin UI login page. To change the realm, set the `realm` property: 
+
+[.dynamic-tabs]
+--
+[example.tab-pane#v1set-property-realm]
+====
+[.tab-label]*V1 API*
+
+[source,bash]
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'Content-type:application/json' -d  '{"set-property": {"realm":"My Solr users"}}'
+----
+====
+
+[example.tab-pane#v2set-property-realm]
+====
+[.tab-label]*V2 API*
+
+[source,bash]
+----
+curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentication -H 'Content-type:application/json' -d  '{"set-property": {"realm":"My Solr users"}}'
+----
+====
+--
+
 == Using Basic Auth with SolrJ
 
 In SolrJ, the basic authentication credentials need to be set for each request as in this example:

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/WEB-INF/web.xml
----------------------------------------------------------------------
diff --git a/solr/webapp/web/WEB-INF/web.xml b/solr/webapp/web/WEB-INF/web.xml
index 155b08b..53ab57a 100644
--- a/solr/webapp/web/WEB-INF/web.xml
+++ b/solr/webapp/web/WEB-INF/web.xml
@@ -33,7 +33,7 @@
     -->
     <init-param>
       <param-name>excludePatterns</param-name>
-      <param-value>/partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/tpl/.+</param-value>
+      <param-value>/partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+</param-value>
     </init-param>
   </filter>
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/css/angular/login.css
----------------------------------------------------------------------
diff --git a/solr/webapp/web/css/angular/login.css b/solr/webapp/web/css/angular/login.css
new file mode 100644
index 0000000..52ad4e0
--- /dev/null
+++ b/solr/webapp/web/css/angular/login.css
@@ -0,0 +1,103 @@
+/*
+
+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.
+
+*/
+
+#content #login
+{
+  background-position: 0 50%;
+  padding-left: 21px;
+  vertical-align: center;
+  horiz-align: center;
+}
+
+#content #login h1,
+#content #login .h1 {
+  font-size: 2.5rem;
+}
+
+#content #login h2, 
+#content #login .h2 {
+  font-size: 2rem;
+}
+
+#content #login p
+{
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+#content #login .login-error
+{
+  font-size: 1rem;
+  color: red;
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+#content #login button {
+  border-radius: 0;
+}
+
+#content #login button:focus {
+  outline: 1px dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+}
+
+#content #login .btn {
+  display: inline-block;
+  font-weight: 400;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: middle;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  border: 1px solid transparent;
+  padding: 0.375rem 0.75rem;
+  font-size: 1rem;
+  line-height: 1.5;
+  border-radius: 0.25rem;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+#content #login .form-inline .form-group {
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  -ms-flex-flow: row wrap;
+  flex-flow: row wrap;
+  -ms-flex-align: center;
+  align-items: center;
+  margin-bottom: 0;
+}
+
+#content #login .form-control {
+  display: block;
+  width: 80%;
+  padding: 0.375rem 0.75rem;
+  font-size: 1rem;
+  line-height: 1.5;
+  color: #495057;
+  background-color: #fff;
+  background-clip: padding-box;
+  border: 1px solid #ced4da;
+  border-radius: 0.25rem;
+  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/css/angular/menu.css
----------------------------------------------------------------------
diff --git a/solr/webapp/web/css/angular/menu.css b/solr/webapp/web/css/angular/menu.css
index 71a1668..4a24399 100644
--- a/solr/webapp/web/css/angular/menu.css
+++ b/solr/webapp/web/css/angular/menu.css
@@ -247,6 +247,8 @@ limitations under the License.
 
 #menu #index.global p a { background-image: url( ../../img/ico/dashboard.png ); }
 
+#menu #login.global p a { background-image: url( ../../img/ico/users.png ); }
+
 #menu #logging.global p a { background-image: url( ../../img/ico/inbox-document-text.png ); }
 #menu #logging.global .level a { background-image: url( ../../img/ico/gear.png ); }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/index.html
----------------------------------------------------------------------
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index 00f70ef..23b9dbd 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -34,6 +34,7 @@ limitations under the License.
   <link rel="stylesheet" type="text/css" href="css/angular/index.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/java-properties.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/logging.css?_=${version}">
+  <link rel="stylesheet" type="text/css" href="css/angular/login.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/menu.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/plugins.css?_=${version}">
   <link rel="stylesheet" type="text/css" href="css/angular/documents.css?_=${version}">
@@ -61,9 +62,11 @@ limitations under the License.
   <script src="libs/highlight.js"></script>
   <script src="libs/d3.js"></script>
   <script src="libs/jquery-ui.min.js"></script>
+  <script src="libs/angular-utf8-base64.min.js"></script>
   <script src="js/angular/app.js"></script>
   <script src="js/angular/services.js"></script>
   <script src="js/angular/controllers/index.js"></script>
+  <script src="js/angular/controllers/login.js"></script>
   <script src="js/angular/controllers/logging.js"></script>
   <script src="js/angular/controllers/cloud.js"></script>
   <script src="js/angular/controllers/collections.js"></script>
@@ -139,88 +142,91 @@ limitations under the License.
         <div>
 
           <ul id="menu">
-
-            <li id="index" class="global" ng-class="{active:page=='index'}"><p><a href="#/">Dashboard</a></p></li>
-
-            <li id="logging" class="global" ng-class="{active:page=='logging'}"><p><a href="#/~logging">Logging</a></p>
-              <ul ng-show="showingLogging">
-                <li class="level" ng-class="{active:page=='logging-levels'}"><a href="#/~logging/level">Level</a></li>
-              </ul>
-            </li>
-
-            <li id="cloud" class="global optional" ng-show="isCloudEnabled" ng-class="{active:showingCloud}"><p><a href="#/~cloud">Cloud</a></p>
-              <ul ng-show="showingCloud">
-                <li class="nodes" ng-class="{active:page=='cloud-nodes'}"><a href="#/~cloud?view=nodes">Nodes</a></li>
-                <li class="tree" ng-class="{active:page=='cloud-tree'}"><a href="#/~cloud?view=tree">Tree</a></li>
-                <li class="zkstatus" ng-class="{active:page=='cloud-zkstatus'}"><a href="#/~cloud?view=zkstatus">ZK Status</a></li>
-                <li class="graph" ng-class="{active:page=='cloud-graph'}"><a href="#/~cloud?view=graph">Graph</a></li>
-              </ul>
-            </li>
-
-            <li ng-show="isCloudEnabled" id="collections" class="global" ng-class="{active:page=='collections'}"><p><a href="#/~collections">Collections</a></p></li>
-            <li ng-hide="isCloudEnabled" id="cores" class="global" ng-class="{active:page=='cores'}"><p><a href="#/~cores">Core Admin</a></p></li>
-
-            <li id="java-properties" class="global" ng-class="{active:page=='java-props'}"><p><a href="#/~java-properties">Java Properties</a></li>
-
-            <li id="threads" class="global" ng-class="{active:page=='threads'}"><p><a href="#/~threads">Thread Dump</a></p></li>
-            <li ng-show="isCloudEnabled" id="cluster-suggestions" class="global" ng-class="{active:page=='cluster-suggestions'}"><p><a href="#/~cluster-suggestions">Suggestions</a></p></li>
-
+            <li id="login" class="global" ng-class="{active:page=='login'}" ng-show="http401 || currentUser"><p><a href="#/login">{{http401 ? "Login" : "Logout " + currentUser}}</a></p></li>
+            
+            <div ng-show="!http401">
+              <li id="index" class="global" ng-class="{active:page=='index'}"><p><a href="#/">Dashboard</a></p></li>
+  
+              <li id="logging" class="global" ng-class="{active:page=='logging'}"><p><a href="#/~logging">Logging</a></p>
+                <ul ng-show="showingLogging">
+                  <li class="level" ng-class="{active:page=='logging-levels'}"><a href="#/~logging/level">Level</a></li>
+                </ul>
+              </li>
+  
+              <li id="cloud" class="global optional" ng-show="isCloudEnabled" ng-class="{active:showingCloud}"><p><a href="#/~cloud">Cloud</a></p>
+                <ul ng-show="showingCloud">
+                  <li class="nodes" ng-class="{active:page=='cloud-nodes'}"><a href="#/~cloud?view=nodes">Nodes</a></li>
+                  <li class="tree" ng-class="{active:page=='cloud-tree'}"><a href="#/~cloud?view=tree">Tree</a></li>
+                  <li class="zkstatus" ng-class="{active:page=='cloud-zkstatus'}"><a href="#/~cloud?view=zkstatus">ZK Status</a></li>
+                  <li class="graph" ng-class="{active:page=='cloud-graph'}"><a href="#/~cloud?view=graph">Graph</a></li>
+                </ul>
+              </li>
+  
+              <li ng-show="isCloudEnabled" id="collections" class="global" ng-class="{active:page=='collections'}"><p><a href="#/~collections">Collections</a></p></li>
+              <li ng-hide="isCloudEnabled" id="cores" class="global" ng-class="{active:page=='cores'}"><p><a href="#/~cores">Core Admin</a></p></li>
+  
+              <li id="java-properties" class="global" ng-class="{active:page=='java-props'}"><p><a href="#/~java-properties">Java Properties</a></li>
+  
+              <li id="threads" class="global" ng-class="{active:page=='threads'}"><p><a href="#/~threads">Thread Dump</a></p></li>
+              <li ng-show="isCloudEnabled" id="cluster-suggestions" class="global" ng-class="{active:page=='cluster-suggestions'}"><p><a href="#/~cluster-suggestions">Suggestions</a></p></li>
+            </div>
           </ul>
 
-          <div id="collection-selector" ng-show="isCloudEnabled">
-            <div id="has-collections" ng-show="collections.length!=0">
-              <select data-placeholder="Collection Selector"
-                      ng-model="currentCollection"
-                      chosen
-                      ng-change="showCollection(currentCollection)"
-                      ng-options="collection.name for collection in collections"></select>
+          <div ng-show="!http401">
+            <div id="collection-selector" ng-show="isCloudEnabled">
+              <div id="has-collections" ng-show="collections.length!=0">
+                <select data-placeholder="Collection Selector"
+                        ng-model="currentCollection"
+                        chosen
+                        ng-change="showCollection(currentCollection)"
+                        ng-options="collection.name for collection in collections"></select>
+              </div>
+              <p id="has-no-collections" ng-show="collections.length==0"><a href="#/~collections">
+                No collections available
+                <span>Go and create one</span>
+              </a></p>
             </div>
-            <p id="has-no-collections" ng-show="collections.length==0"><a href="#/~collections">
-              No collections available
-              <span>Go and create one</span>
-            </a></p>
-          </div>
-          <div id="collection-menu" class="sub-menu" ng-show="currentCollection">
-            <ul>
-              <li class="overview" ng-class="{active:page=='collection-overview'}"><a href="#/{{currentCollection.name}}/collection-overview"><span>Overview</span></a></li>
-              <li class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCollection.name}}/analysis"><span>Analysis</span></a></li>
-              <li class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCollection.name}}/dataimport"><span>Dataimport</span></a></li>
-              <li class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCollection.name}}/documents"><span>Documents</span></a></li>
-              <li class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCollection.name}}/files"><span>Files</span></a></li>
-              <li class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCollection.name}}/query"><span>Query</span></a></li>
-              <li class="stream" ng-class="{active:page=='stream'}"><a href="#/{{currentCollection.name}}/stream"><span>Stream</span></a></li>
-              <li class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCollection.name}}/schema"><span>Schema</span></a></li>
-        </ul>
-          </div>
-          <div id="core-selector">
-            <div id="has-cores" ng-show="cores.length!=0">
-              <select data-placeholder="Core Selector"
-                      ng-model="currentCore"
-                      chosen
-                      ng-change="showCore(currentCore)"
-                      ng-options="core.name for core in cores"></select>
+            <div id="collection-menu" class="sub-menu" ng-show="currentCollection">
+              <ul>
+                <li class="overview" ng-class="{active:page=='collection-overview'}"><a href="#/{{currentCollection.name}}/collection-overview"><span>Overview</span></a></li>
+                <li class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCollection.name}}/analysis"><span>Analysis</span></a></li>
+                <li class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCollection.name}}/dataimport"><span>Dataimport</span></a></li>
+                <li class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCollection.name}}/documents"><span>Documents</span></a></li>
+                <li class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCollection.name}}/files"><span>Files</span></a></li>
+                <li class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCollection.name}}/query"><span>Query</span></a></li>
+                <li class="stream" ng-class="{active:page=='stream'}"><a href="#/{{currentCollection.name}}/stream"><span>Stream</span></a></li>
+                <li class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCollection.name}}/schema"><span>Schema</span></a></li>
+              </ul>
+            </div>
+            <div id="core-selector">
+              <div id="has-cores" ng-show="cores.length!=0">
+                <select data-placeholder="Core Selector"
+                        ng-model="currentCore"
+                        chosen
+                        ng-change="showCore(currentCore)"
+                        ng-options="core.name for core in cores"></select>
+              </div>
+              <p id="has-no-cores" ng-show="cores.length==0"><a href="#/~cores">
+                No cores available
+                <span>Go and create one</span>
+              </a></p>
+            </div>
+            <div id="core-menu" class="sub-menu" ng-show="currentCore">
+              <ul>
+                <li class="overview" ng-class="{active:page=='overview'}"><a href="#/{{currentCore.name}}/core-overview"><span>Overview</span></a></li>
+                <li ng-hide="isCloudEnabled" class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCore.name}}/analysis"><span>Analysis</span></a></li>
+                <li ng-hide="isCloudEnabled" class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCore.name}}/dataimport"><span>Dataimport</span></a></li>
+                <li ng-hide="isCloudEnabled" class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCore.name}}/documents"><span>Documents</span></a></li>
+                <li ng-hide="isCloudEnabled" class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCore.name}}/files"><span>Files</span></a></li>
+                <li class="ping" ng-class="{active:page=='ping'}"><a ng-click="ping()"><span>Ping</span><small class="qtime" ng-show="showPing"> (<span>{{pingMS}}ms</span>)</small></a></li>
+                <li class="plugins" ng-class="{active:page=='plugins'}"><a href="#/{{currentCore.name}}/plugins"><span>Plugins / Stats</span></a></li>
+                <li ng-hide="isCloudEnabled" class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCore.name}}/query"><span>Query</span></a></li>
+                <li ng-hide="isCloudEnabled" class="replication" ng-class="{active:page=='replication'}"><a href="#/{{currentCore.name}}/replication"><span>Replication</span></a></li>
+                <li ng-hide="isCloudEnabled" class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCore.name}}/schema"><span>Schema</span></a></li>
+                <li class="segments" ng-class="{active:page=='segments'}"><a href="#/{{currentCore.name}}/segments"><span>Segments info</span></a></li>
+              </ul>
             </div>
-            <p id="has-no-cores" ng-show="cores.length==0"><a href="#/~cores">
-              No cores available
-              <span>Go and create one</span>
-            </a></p>
-          </div>
-          <div id="core-menu" class="sub-menu" ng-show="currentCore">
-            <ul>
-              <li class="overview" ng-class="{active:page=='overview'}"><a href="#/{{currentCore.name}}"><span>Overview</span></a></li>
-              <li ng-hide="isCloudEnabled" class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCore.name}}/analysis"><span>Analysis</span></a></li>
-              <li ng-hide="isCloudEnabled" class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCore.name}}/dataimport"><span>Dataimport</span></a></li>
-              <li ng-hide="isCloudEnabled" class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCore.name}}/documents"><span>Documents</span></a></li>
-              <li ng-hide="isCloudEnabled" class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCore.name}}/files"><span>Files</span></a></li>
-              <li class="ping" ng-class="{active:page=='ping'}"><a ng-click="ping()"><span>Ping</span><small class="qtime" ng-show="showPing"> (<span>{{pingMS}}ms</span>)</small></a></li>
-              <li class="plugins" ng-class="{active:page=='plugins'}"><a href="#/{{currentCore.name}}/plugins"><span>Plugins / Stats</span></a></li>
-              <li ng-hide="isCloudEnabled" class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCore.name}}/query"><span>Query</span></a></li>
-              <li ng-hide="isCloudEnabled" class="replication" ng-class="{active:page=='replication'}"><a href="#/{{currentCore.name}}/replication"><span>Replication</span></a></li>
-              <li ng-hide="isCloudEnabled" class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCore.name}}/schema"><span>Schema</span></a></li>
-              <li class="segments" ng-class="{active:page=='segments'}"><a href="#/{{currentCore.name}}/segments"><span>Segments info</span></a></li>
-      </ul>
           </div>
-
         </div>
       </div>
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/js/angular/app.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js
index ad96ce0..cb04ba3 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -21,7 +21,8 @@ var solrAdminApp = angular.module("solrAdminApp", [
   "ngCookies",
   "ngtimeago",
   "solrAdminServices",
-  "localytics.directives"
+  "localytics.directives",
+  "ab-base64"
 ]);
 
 solrAdminApp.config([
@@ -31,6 +32,10 @@ solrAdminApp.config([
         templateUrl: 'partials/index.html',
         controller: 'IndexController'
       }).
+      when('/login', {
+        templateUrl: 'partials/login.html',
+        controller: 'LoginController'
+      }).
       when('/~logging', {
         templateUrl: 'partials/logging.html',
         controller: 'LoggingController'
@@ -315,7 +320,7 @@ solrAdminApp.config([
     }
   };
 })
-.factory('httpInterceptor', function($q, $rootScope, $timeout, $injector) {
+.factory('httpInterceptor', function($q, $rootScope, $location, $timeout, $injector) {
   var activeRequests = 0;
 
   var started = function(config) {
@@ -326,6 +331,9 @@ solrAdminApp.config([
       delete $rootScope.exceptions[config.url];
     }
     activeRequests++;
+    if (sessionStorage.getItem("auth.header")) {
+      config.headers['Authorization'] = sessionStorage.getItem("auth.header");
+    }
     config.timeout = 10000;
     return config || $q.when(config);
   };
@@ -343,6 +351,11 @@ solrAdminApp.config([
         $rootScope.$broadcast('connectionStatusInactive');
       },2000);
     }
+    if (!$location.path().startsWith('/login')) {
+      sessionStorage.removeItem("http401");
+      sessionStorage.removeItem("auth.state");
+      sessionStorage.removeItem("auth.statusText");
+    }
     return response || $q.when(response);
   };
 
@@ -361,16 +374,38 @@ solrAdminApp.config([
       var $http = $injector.get('$http');
       var result = $http(rejection.config);
       return result;
+    } else if (rejection.status === 401) {
+      // Authentication redirect
+      var headers = rejection.headers();
+      var wwwAuthHeader = headers['www-authenticate'];
+      sessionStorage.setItem("auth.wwwAuthHeader", wwwAuthHeader);
+      sessionStorage.setItem("auth.statusText", rejection.statusText);
+      sessionStorage.setItem("http401", "true");
+      sessionStorage.removeItem("auth.scheme");
+      sessionStorage.removeItem("auth.realm");
+      sessionStorage.removeItem("auth.username");
+      sessionStorage.removeItem("auth.header");
+      sessionStorage.removeItem("auth.state");
+      if ($location.path().includes('/login')) {
+        if (!sessionStorage.getItem("auth.location")) {
+          sessionStorage.setItem("auth.location", "/");
+        }
+      } else {
+        sessionStorage.setItem("auth.location", $location.path());
+        $location.path('/login');
+      }
     } else {
       $rootScope.exceptions[rejection.config.url] = rejection.data.error;
     }
     return $q.reject(rejection);
-  }
+  };
 
   return {request: started, response: ended, responseError: failed};
 })
 .config(function($httpProvider) {
   $httpProvider.interceptors.push("httpInterceptor");
+  // Force BasicAuth plugin to serve us a 'Authorization: xBasic xxxx' header so browser will not pop up login dialogue
+  $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 })
 .directive('fileModel', function ($parse) {
     return {
@@ -441,6 +476,8 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
     $scope.showingLogging = page.lastIndexOf("logging", 0) === 0;
     $scope.showingCloud = page.lastIndexOf("cloud", 0) === 0;
     $scope.page = page;
+    $scope.currentUser = sessionStorage.getItem("auth.username");
+    $scope.http401 = sessionStorage.getItem("http401");
   };
 
   $scope.ping = function() {
@@ -456,7 +493,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
   }
 
   $scope.showCore = function(core) {
-    $location.url("/" + core.name);
+    $location.url("/" + core.name + "/core-overview");
   }
 
   $scope.showCollection = function(collection) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/js/angular/controllers/login.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js
new file mode 100644
index 0000000..9935191
--- /dev/null
+++ b/solr/webapp/web/js/angular/controllers/login.js
@@ -0,0 +1,146 @@
+/*
+ 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.
+*/
+
+solrAdminApp.controller('LoginController',
+    ['$scope', '$routeParams', '$rootScope', '$location', '$window', 'AuthenticationService', 'Constants',
+      function ($scope, $routeParams, $rootScope, $location, $window, AuthenticationService, Constants) {
+        $scope.resetMenu("login", Constants.IS_ROOT_PAGE);
+        $scope.subPath = $routeParams.route;
+        $rootScope.exceptions = {};
+
+        // Session variables set in app.js 401 interceptor
+        var wwwAuthHeader = sessionStorage.getItem("auth.wwwAuthHeader");
+        var authScheme = sessionStorage.getItem("auth.scheme");
+        if (wwwAuthHeader) {
+          // Parse www-authenticate header
+          var wwwHeader = wwwAuthHeader.match(/(\w+)\s+(.*)/);
+          authScheme = wwwHeader[1];
+          var authParams = www_auth_parse_params(wwwHeader[2]);
+          if (typeof authParams === 'string' || authParams instanceof String) {
+            $scope.authParamsError = authParams;
+          } else {
+            $scope.authParamsError = undefined;
+          }
+          var realm = authParams['realm'];
+          sessionStorage.setItem("auth.realm", realm);
+          if (authScheme === 'Basic' || authScheme === 'xBasic') {
+            authScheme = 'Basic';
+          }
+          sessionStorage.setItem("auth.scheme", authScheme);
+        }
+
+        var supportedSchemes = ['Basic', 'Bearer'];
+        $scope.authSchemeSupported = supportedSchemes.includes(authScheme);
+        $scope.authScheme = sessionStorage.getItem("auth.scheme");
+        $scope.authRealm = sessionStorage.getItem("auth.realm");
+        $scope.wwwAuthHeader = sessionStorage.getItem("auth.wwwAuthHeader");
+        $scope.statusText = sessionStorage.getItem("auth.statusText");
+        $scope.authConfig = sessionStorage.getItem("auth.config");
+        $scope.authLocation = sessionStorage.getItem("auth.location");
+        $scope.authLoggedinUser = sessionStorage.getItem("auth.username");
+        $scope.authHeader = sessionStorage.getItem("auth.header");
+
+        $scope.login = function () {
+          AuthenticationService.SetCredentials($scope.username, $scope.password);
+          $location.path($scope.authLocation); // Redirect to the location that caused the login prompt
+        };
+
+        $scope.logout = function() {
+          // reset login status
+          AuthenticationService.ClearCredentials();
+          $location.path("/");
+        };
+
+        $scope.isLoggedIn = function() {
+          return (sessionStorage.getItem("auth.username") !== null);
+        };
+      }]);
+
+// This function is copied and adapted from MIT-licensed https://github.com/randymized/www-authenticate/blob/master/lib/parsers.js
+www_auth_parse_params= function (header) {
+  // This parser will definitely fail if there is more than one challenge
+  var params = {};
+  var tok, last_tok, _i, _len, key, value;
+  var state= 0;   //0: token,
+  var m= header.split(/([",=])/);
+  for (_i = 0, _len = m.length; _i < _len; _i++) {
+    last_tok= tok;
+    tok = m[_i];
+    if (!tok.length) continue;
+    switch (state) {
+      case 0: // token
+        key= tok.trim();
+        state= 1; // expect equals
+        continue;
+      case 1: // expect equals
+        if ('=' != tok) return 'Equal sign was expected after '+key;
+        state= 2;
+        continue;
+      case 2: // expect value
+        if ('"' == tok) {
+          value= '';
+          state= 3; // expect quoted
+          continue;
+        }
+        else {
+          params[key]= value= tok.trim();
+          state= 9; // expect comma or end
+          continue;
+        }
+      case 3: // handling quoted string
+        if ('"' == tok) {
+          state= 8; // end quoted
+          continue;
+        }
+        else {
+          value+= tok;
+          state= 3; // continue accumulating quoted string
+          continue;
+        }
+      case 8: // end quote encountered
+        if ('"' == tok) {
+          // double quoted
+          value+= '"';
+          state= 3; // back to quoted string
+          continue;
+        }
+        if (',' == tok) {
+          params[key]= value;
+          state= 0;
+          continue;
+        }
+        else {
+          return 'Unexpected token ('+tok+') after '+value+'"';
+        }
+        continue;
+      case 9: // expect commma
+        if (',' != tok) return 'Comma expected after '+value;
+        state= 0;
+        continue;
+    }
+  }
+  switch (state) {  // terminal state
+    case 0:   // Empty or ignoring terminal comma
+    case 9:   // Expecting comma or end of header
+      return params;
+    case 8:   // Last token was end quote
+      params[key]= value;
+      return params;
+    default:
+      return 'Unexpected end of www-authenticate value.';
+  }
+};

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/js/angular/services.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index e3dcd3f..8eb148f 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -262,4 +262,26 @@ solrAdminServices.factory('System',
      return $resource(':core/config', {wt: 'json', core: '@core', _:Date.now()}, {
        get: {method: "GET"}
      })
-}]);
+}])
+.factory('AuthenticationService',
+    ['base64', function (base64) {
+        var service = {};
+
+        service.SetCredentials = function (username, password) {
+          var authdata = base64.encode(username + ':' + password);
+
+          sessionStorage.setItem("auth.header", "Basic " + authdata);
+          sessionStorage.setItem("auth.username", username);
+        };
+
+        service.ClearCredentials = function () {
+          sessionStorage.removeItem("auth.header");
+          sessionStorage.removeItem("auth.scheme");
+          sessionStorage.removeItem("auth.realm");
+          sessionStorage.removeItem("auth.username");
+          sessionStorage.removeItem("auth.wwwAuthHeader");
+          sessionStorage.removeItem("auth.statusText");
+        };
+
+        return service;
+      }]);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/libs/angular-utf8-base64.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/libs/angular-utf8-base64.js b/solr/webapp/web/libs/angular-utf8-base64.js
new file mode 100755
index 0000000..a3a7358
--- /dev/null
+++ b/solr/webapp/web/libs/angular-utf8-base64.js
@@ -0,0 +1,217 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2014 Andrey Bezyazychniy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+'use strict';
+
+angular.module('ab-base64',[]).constant('base64', (function() {
+
+    /*
+     * Encapsulation of Vassilis Petroulias's base64.js library for AngularJS
+     * Original notice included below
+     */
+
+    /*
+     Copyright Vassilis Petroulias [DRDigit]
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+     */
+    var B64 = {
+        alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
+        lookup: null,
+        ie: /MSIE /.test(navigator.userAgent),
+        ieo: /MSIE [67]/.test(navigator.userAgent),
+        encode: function (s) {
+            /* jshint bitwise:false */
+            var buffer = B64.toUtf8(s),
+                position = -1,
+                result,
+                len = buffer.length,
+                nan0, nan1, nan2, enc = [, , , ];
+            
+            if (B64.ie) {
+                result = [];
+                while (++position < len) {
+                    nan0 = buffer[position];
+                    nan1 = buffer[++position];
+                    enc[0] = nan0 >> 2;
+                    enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4);
+                    if (isNaN(nan1))
+                        enc[2] = enc[3] = 64;
+                    else {
+                        nan2 = buffer[++position];
+                        enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6);
+                        enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63;
+                    }
+                    result.push(B64.alphabet.charAt(enc[0]), B64.alphabet.charAt(enc[1]), B64.alphabet.charAt(enc[2]), B64.alphabet.charAt(enc[3]));
+                }
+                return result.join('');
+            } else {
+                result = '';
+                while (++position < len) {
+                    nan0 = buffer[position];
+                    nan1 = buffer[++position];
+                    enc[0] = nan0 >> 2;
+                    enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4);
+                    if (isNaN(nan1))
+                        enc[2] = enc[3] = 64;
+                    else {
+                        nan2 = buffer[++position];
+                        enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6);
+                        enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63;
+                    }
+                    result += B64.alphabet[enc[0]] + B64.alphabet[enc[1]] + B64.alphabet[enc[2]] + B64.alphabet[enc[3]];
+                }
+                return result;
+            }
+        },
+        decode: function (s) {
+            /* jshint bitwise:false */
+            s = s.replace(/\s/g, '');
+            if (s.length % 4)
+                throw new Error('InvalidLengthError: decode failed: The string to be decoded is not the correct length for a base64 encoded string.');
+            if(/[^A-Za-z0-9+\/=\s]/g.test(s))
+                throw new Error('InvalidCharacterError: decode failed: The string contains characters invalid in a base64 encoded string.');
+
+            var buffer = B64.fromUtf8(s),
+                position = 0,
+                result,
+                len = buffer.length;
+
+            if (B64.ieo) {
+                result = [];
+                while (position < len) {
+                    if (buffer[position] < 128)
+                        result.push(String.fromCharCode(buffer[position++]));
+                    else if (buffer[position] > 191 && buffer[position] < 224)
+                        result.push(String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63)));
+                    else
+                        result.push(String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63)));
+                }
+                return result.join('');
+            } else {
+                result = '';
+                while (position < len) {
+                    if (buffer[position] < 128)
+                        result += String.fromCharCode(buffer[position++]);
+                    else if (buffer[position] > 191 && buffer[position] < 224)
+                        result += String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63));
+                    else
+                        result += String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63));
+                }
+                return result;
+            }
+        },
+        toUtf8: function (s) {
+            /* jshint bitwise:false */
+            var position = -1,
+                len = s.length,
+                chr, buffer = [];
+            if (/^[\x00-\x7f]*$/.test(s)) while (++position < len)
+                buffer.push(s.charCodeAt(position));
+            else while (++position < len) {
+                chr = s.charCodeAt(position);
+                if (chr < 128)
+                    buffer.push(chr);
+                else if (chr < 2048)
+                    buffer.push((chr >> 6) | 192, (chr & 63) | 128);
+                else
+                    buffer.push((chr >> 12) | 224, ((chr >> 6) & 63) | 128, (chr & 63) | 128);
+            }
+            return buffer;
+        },
+        fromUtf8: function (s) {
+            /* jshint bitwise:false */
+            var position = -1,
+                len, buffer = [],
+                enc = [, , , ];
+            if (!B64.lookup) {
+                len = B64.alphabet.length;
+                B64.lookup = {};
+                while (++position < len)
+                    B64.lookup[B64.alphabet.charAt(position)] = position;
+                position = -1;
+            }
+            len = s.length;
+            while (++position < len) {
+                enc[0] = B64.lookup[s.charAt(position)];
+                enc[1] = B64.lookup[s.charAt(++position)];
+                buffer.push((enc[0] << 2) | (enc[1] >> 4));
+                enc[2] = B64.lookup[s.charAt(++position)];
+                if (enc[2] === 64)
+                    break;
+                buffer.push(((enc[1] & 15) << 4) | (enc[2] >> 2));
+                enc[3] = B64.lookup[s.charAt(++position)];
+                if (enc[3] === 64)
+                    break;
+                buffer.push(((enc[2] & 3) << 6) | enc[3]);
+            }
+            return buffer;
+        }
+    };
+
+    var B64url = {
+        decode: function(input) {
+            // Replace non-url compatible chars with base64 standard chars
+            input = input
+                .replace(/-/g, '+')
+                .replace(/_/g, '/');
+
+            // Pad out with standard base64 required padding characters
+            var pad = input.length % 4;
+            if(pad) {
+              if(pad === 1) {
+                throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
+              }
+              input += new Array(5-pad).join('=');
+            }
+
+            return B64.decode(input);
+        },
+
+        encode: function(input) {
+            var output = B64.encode(input);
+            return output
+                .replace(/\+/g, '-')
+                .replace(/\//g, '_')
+                .split('=', 1)[0];
+        }
+    };
+
+    return {
+        decode: B64.decode,
+        encode: B64.encode,
+        urldecode: B64url.decode,
+        urlencode: B64url.encode,
+    };
+})());
+

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/libs/angular-utf8-base64.min.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/libs/angular-utf8-base64.min.js b/solr/webapp/web/libs/angular-utf8-base64.min.js
new file mode 100755
index 0000000..e1166c6
--- /dev/null
+++ b/solr/webapp/web/libs/angular-utf8-base64.min.js
@@ -0,0 +1,45 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2014 Andrey Bezyazychniy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/*
+ * Encapsulation of Vassilis Petroulias's base64.js library for AngularJS
+ * Original notice included below
+ */
+
+/*
+ Copyright Vassilis Petroulias [DRDigit]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+"use strict";angular.module("ab-base64",[]).constant("base64",function(){var a={alphabet:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",lookup:null,ie:/MSIE /.test(navigator.userAgent),ieo:/MSIE [67]/.test(navigator.userAgent),encode:function(b){var c,d,e,f,g=a.toUtf8(b),h=-1,i=g.length,j=[,,,];if(a.ie){for(c=[];++h<i;)d=g[h],e=g[++h],j[0]=d>>2,j[1]=(3&d)<<4|e>>4,isNaN(e)?j[2]=j[3]=64:(f=g[++h],j[2]=(15&e)<<2|f>>6,j[3]=isNaN(f)?64:63&f),c.push(a.alphabet.charAt(j[0]),a.alphabet.charAt(j[1]),a.alphabet.charAt(j[2]),a.alphabet.charAt(j[3]));return c.join("")}for(c="";++h<i;)d=g[h],e=g[++h],j[0]=d>>2,j[1]=(3&d)<<4|e>>4,isNaN(e)?j[2]=j[3]=64:(f=g[++h],j[2]=(15&e)<<2|f>>6,j[3]=isNaN(f)?64:63&f),c+=a.alphabet[j[0]]+a.alphabet[j[1]]+a.alphabet[j[2]]+a.alphabet[j[3]];return c},decode:function(b){if(b=b.replace(/\s/g,""),b.length%4)throw new Error("InvalidLengthError: decode failed: The string to be decoded is not the correct length for a base64 encoded string.");if(/[^A
 -Za-z0-9+\/=\s]/g.test(b))throw new Error("InvalidCharacterError: decode failed: The string contains characters invalid in a base64 encoded string.");var c,d=a.fromUtf8(b),e=0,f=d.length;if(a.ieo){for(c=[];f>e;)c.push(d[e]<128?String.fromCharCode(d[e++]):d[e]>191&&d[e]<224?String.fromCharCode((31&d[e++])<<6|63&d[e++]):String.fromCharCode((15&d[e++])<<12|(63&d[e++])<<6|63&d[e++]));return c.join("")}for(c="";f>e;)c+=String.fromCharCode(d[e]<128?d[e++]:d[e]>191&&d[e]<224?(31&d[e++])<<6|63&d[e++]:(15&d[e++])<<12|(63&d[e++])<<6|63&d[e++]);return c},toUtf8:function(a){var b,c=-1,d=a.length,e=[];if(/^[\x00-\x7f]*$/.test(a))for(;++c<d;)e.push(a.charCodeAt(c));else for(;++c<d;)b=a.charCodeAt(c),128>b?e.push(b):2048>b?e.push(b>>6|192,63&b|128):e.push(b>>12|224,b>>6&63|128,63&b|128);return e},fromUtf8:function(b){var c,d=-1,e=[],f=[,,,];if(!a.lookup){for(c=a.alphabet.length,a.lookup={};++d<c;)a.lookup[a.alphabet.charAt(d)]=d;d=-1}for(c=b.length;++d<c&&(f[0]=a.lookup[b.charAt(d)],f[1]=a.lookup[
 b.charAt(++d)],e.push(f[0]<<2|f[1]>>4),f[2]=a.lookup[b.charAt(++d)],64!==f[2])&&(e.push((15&f[1])<<4|f[2]>>2),f[3]=a.lookup[b.charAt(++d)],64!==f[3]);)e.push((3&f[2])<<6|f[3]);return e}},b={decode:function(b){b=b.replace(/-/g,"+").replace(/_/g,"/");var c=b.length%4;if(c){if(1===c)throw new Error("InvalidLengthError: Input base64url string is the wrong length to determine padding");b+=new Array(5-c).join("=")}return a.decode(b)},encode:function(b){var c=a.encode(b);return c.replace(/\+/g,"-").replace(/\//g,"_").split("=",1)[0]}};return{decode:a.decode,encode:a.encode,urldecode:b.decode,urlencode:b.encode}}());
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/280f6792/solr/webapp/web/partials/login.html
----------------------------------------------------------------------
diff --git a/solr/webapp/web/partials/login.html b/solr/webapp/web/partials/login.html
new file mode 100644
index 0000000..10a3caf
--- /dev/null
+++ b/solr/webapp/web/partials/login.html
@@ -0,0 +1,80 @@
+<!--
+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.
+-->
+<div id="login" class="clearfix">
+
+  <div ng-show="authScheme === 'Basic'">
+    <h1>Basic Authentication</h1>
+    <div class="login-error" ng-show="statusText !== 'require authentication' || authParamsError !== null">
+      {{statusText}}{{authParamsError}}
+    </div>
+    <div ng-show="!isLoggedIn()">
+      <p>
+        Solr requires authentication for resource {{authLocation === '/' ? 'Dashboard' : authLocation}}.<br/>
+        Please log in with your username and password for realm {{authRealm}}.
+      </p>
+      <br/>
+      <div ng-show="error" class="alert alert-danger">{{error}}</div>
+      <form name="form" ng-submit="login()" role="form">
+        <div class="form-group">
+          <label for="username">Username</label>
+          <input type="text" name="username" id="username" class="form-control" ng-model="username" required />
+          <span ng-show="form.username.$dirty && form.username.$error.required" class="help-block">Username is required</span>
+        </div>
+        <div class="form-group">
+          <label for="password">Password</label>
+          <input type="password" name="password" id="password" class="form-control" ng-model="password" required />
+          <span ng-show="form.password.$dirty && form.password.$error.required" class="help-block">Password is required</span>
+        </div>
+        <br/>
+        <div class="form-actions">
+          <button type="submit" ng-disabled="form.$invalid" class="btn btn-danger">Login</button>
+        </div>
+      </form>
+    </div>
+
+    <div ng-show="isLoggedIn()">
+      <p>
+        Logged in as user {{authLoggedinUser}}. Realm={{authRealm}}.<br/>
+      </p>
+      <br/>
+      <form name="logoutForm" ng-submit="logout()" role="form" ng-show="isLoggedIn()">
+        <div class="form-actions">
+          <button type="submit" class="btn btn-danger">Logout</button>
+        </div>
+      </form>
+    </div>
+
+  </div>
+
+
+  <div ng-show="!authSchemeSupported">
+    <h1>Authentication scheme not supported</h1>
+
+    <div class="login-error">
+      {{statusText}}
+    </div>
+    
+    <p>Some or all Solr operations are protected by an authentication scheme that is not yet supported by this Admin UI ({{authScheme}}).</p>
+    <p>Solr returned an error response:
+    <hr/>
+    <pre>HTTP 401 {{statusText}}
+WWW-Authenticate: {{wwwAuthHeader}}</pre>
+    <hr/>
+    </p>
+    <p>A possible workaround may be to use another client that supports this scheme.</p>
+  </div>
+</div>