8138990: Implementation of HTTP Digest authentication may be more flexible
authorasmotrak
Thu, 21 Jan 2016 09:15:31 -0800
changeset 35306 74feccca64cc
parent 35305 500c5fc13aa4
child 35307 50b5ac3cfb59
8138990: Implementation of HTTP Digest authentication may be more flexible Reviewed-by: michaelm
jdk/src/java.base/share/classes/sun/net/www/protocol/http/DigestAuthentication.java
jdk/test/sun/net/www/http/HttpURLConnection/DigestAuth.java
--- a/jdk/src/java.base/share/classes/sun/net/www/protocol/http/DigestAuthentication.java	Thu Jan 21 10:31:45 2016 +0000
+++ b/jdk/src/java.base/share/classes/sun/net/www/protocol/http/DigestAuthentication.java	Thu Jan 21 09:15:31 2016 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -30,7 +30,6 @@
 import java.net.ProtocolException;
 import java.net.PasswordAuthentication;
 import java.util.Arrays;
-import java.util.StringTokenizer;
 import java.util.Random;
 
 import sun.net.www.HeaderParser;
@@ -146,9 +145,9 @@
 
         synchronized void setQop (String qop) {
             if (qop != null) {
-                StringTokenizer st = new StringTokenizer (qop, " ");
-                while (st.hasMoreTokens()) {
-                    if (st.nextToken().equalsIgnoreCase ("auth")) {
+                String items[] = qop.split(",");
+                for (String item : items) {
+                    if ("auth".equalsIgnoreCase(item.trim())) {
                         serverQop = true;
                         return;
                     }
@@ -163,7 +162,7 @@
         synchronized String getNonce () { return nonce;}
 
         synchronized void setNonce (String s) {
-            if (!s.equals(nonce)) {
+            if (nonce == null || !s.equals(nonce)) {
                 nonce=s;
                 NCcount = 0;
                 redoCachedHA1 = true;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/sun/net/www/http/HttpURLConnection/DigestAuth.java	Thu Jan 21 09:15:31 2016 -0800
@@ -0,0 +1,424 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Authenticator;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+
+/*
+ * @test
+ * @bug 8138990
+ * @summary Tests for HTTP Digest auth
+ *          The impl maintains a cache for auth info,
+ *          the testcases run in a separate JVM to avoid cache hits
+ * @run main/othervm DigestAuth good
+ * @run main/othervm DigestAuth only_nonce
+ * @run main/othervm DigestAuth sha1
+ * @run main/othervm DigestAuth no_header
+ * @run main/othervm DigestAuth no_nonce
+ * @run main/othervm DigestAuth no_qop
+ * @run main/othervm DigestAuth invalid_alg
+ * @run main/othervm DigestAuth validate_server
+ * @run main/othervm DigestAuth validate_server_no_qop
+ */
+public class DigestAuth {
+
+    static final String LOCALHOST = "localhost";
+    static final String EXPECT_FAILURE = null;
+    static final String EXPECT_DIGEST = "Digest";
+    static final String REALM = "testrealm@host.com";
+    static final String NEXT_NONCE = "40f2e879449675f288476d772627370a";
+
+    static final String GOOD_WWW_AUTH_HEADER = "Digest "
+            + "realm=\"testrealm@host.com\", "
+            + "qop=\"auth,auth-int\", "
+            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
+
+    static final String GOOD_WWW_AUTH_HEADER_NO_QOP = "Digest "
+            + "realm=\"testrealm@host.com\", "
+            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
+
+    static final String WWW_AUTH_HEADER_NO_NONCE = "Digest "
+            + "realm=\"testrealm@host.com\", "
+            + "qop=\"auth,auth-int\", "
+            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
+
+    static final String WWW_AUTH_HEADER_NO_QOP = "Digest "
+            + "realm=\"testrealm@host.com\", "
+            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
+
+    static final String WWW_AUTH_HEADER_ONLY_NONCE = "Digest "
+            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"";
+
+    static final String WWW_AUTH_HEADER_SHA1 = "Digest "
+            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+            + "algorithm=\"SHA1\"";
+
+    static final String WWW_AUTH_HEADER_INVALID_ALGORITHM = "Digest "
+            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+            + "algorithm=\"SHA123\"";
+
+    static final String AUTH_INFO_HEADER_NO_QOP_FIRST =
+              "nextnonce=\"" + NEXT_NONCE + "\", "
+            + "rspauth=\"ee85bc4315d8b18757809f1a8b9382d8\"";
+
+    static final String AUTH_INFO_HEADER_NO_QOP_SECOND =
+              "rspauth=\"12f2fa12841b3775b6054576722446b2\"";
+
+    static final String AUTH_INFO_HEADER_WRONG_DIGEST =
+              "nextnonce=\"" + NEXT_NONCE + "\", "
+            + "rspauth=\"7327570c586207eca2afae94fc20903d\", "
+            + "cnonce=\"0a4f113b\", "
+            + "nc=00000001, "
+            + "qop=auth";
+
+    public static void main(String[] args) throws Exception {
+        if (args.length == 0) {
+            throw new RuntimeException("No testcase specified");
+        }
+        String testcase = args[0];
+
+        // start a local HTTP server
+        try (LocalHttpServer server = LocalHttpServer.startServer()) {
+
+            // set authenticator
+            AuthenticatorImpl auth = new AuthenticatorImpl();
+            Authenticator.setDefault(auth);
+
+            String url = String.format("http://%s:%d/test/",
+                    LOCALHOST, server.getPort());
+
+            boolean success = true;
+            switch (testcase) {
+                case "good":
+                    // server returns a good WWW-Authenticate header
+                    server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
+                    success = testAuth(url, auth, EXPECT_DIGEST);
+                    if (auth.lastRequestedPrompt == null ||
+                            !auth.lastRequestedPrompt.equals(REALM)) {
+                        System.out.println("Unexpected realm: "
+                                + auth.lastRequestedPrompt);
+                        success = false;
+                    }
+                    break;
+                case "validate_server":
+                    // enable processing Authentication-Info headers
+                    System.setProperty("http.auth.digest.validateServer",
+                            "true");
+
+                    /* Server returns good WWW-Authenticate
+                     * and Authentication-Info headers with wrong digest
+                     */
+                    server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
+                    server.setAuthInfoHeader(AUTH_INFO_HEADER_WRONG_DIGEST);
+                    success = testAuth(url, auth, EXPECT_FAILURE);
+                    if (auth.lastRequestedPrompt == null ||
+                            !auth.lastRequestedPrompt.equals(REALM)) {
+                        System.out.println("Unexpected realm: "
+                                + auth.lastRequestedPrompt);
+                        success = false;
+                    }
+                    break;
+                case "validate_server_no_qop":
+                    // enable processing Authentication-Info headers
+                    System.setProperty("http.auth.digest.validateServer",
+                            "true");
+
+                    /* Server returns good both WWW-Authenticate
+                     * and Authentication-Info headers without any qop field,
+                     * so that client-nonce should not be taked into account,
+                     * and connection should succeed.
+                     */
+                    server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER_NO_QOP);
+                    server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_FIRST);
+                    success = testAuth(url, auth, EXPECT_DIGEST);
+                    if (auth.lastRequestedPrompt == null ||
+                            !auth.lastRequestedPrompt.equals(REALM)) {
+                        System.out.println("Unexpected realm: "
+                                + auth.lastRequestedPrompt);
+                        success = false;
+                    }
+
+                    // connect again and check if nextnonce was used
+                    server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_SECOND);
+                    success &= testAuth(url, auth, EXPECT_DIGEST);
+                    if (!NEXT_NONCE.equals(server.lastRequestedNonce)) {
+                        System.out.println("Unexpected next nonce: "
+                                + server.lastRequestedNonce);
+                        success = false;
+                    }
+                    break;
+                case "only_nonce":
+                    /* Server returns a good WWW-Authenticate header
+                     * which contains only nonce (no realm set).
+                     *
+                     * Realm from  WWW-Authenticate header is passed to
+                     * authenticator which can use it as a prompt
+                     * when it asks a user for credentials.
+                     *
+                     * It's fine if an HTTP client doesn't fail if no realm set,
+                     * and delegates making a decision to authenticator/user.
+                     */
+                    server.setWWWAuthHeader(WWW_AUTH_HEADER_ONLY_NONCE);
+                    success = testAuth(url, auth, EXPECT_DIGEST);
+                    if (auth.lastRequestedPrompt != null &&
+                            !auth.lastRequestedPrompt.trim().isEmpty()) {
+                        System.out.println("Unexpected realm: "
+                                + auth.lastRequestedPrompt);
+                        success = false;
+                    }
+                    break;
+                case "sha1":
+                    // server returns a good WWW-Authenticate header with SHA-1
+                    server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1);
+                    success = testAuth(url, auth, EXPECT_DIGEST);
+                    break;
+                case "no_header":
+                    // server returns no WWW-Authenticate header
+                    success = testAuth(url, auth, EXPECT_FAILURE);
+                    if (auth.lastRequestedScheme != null) {
+                        System.out.println("Unexpected scheme: "
+                                + auth.lastRequestedScheme);
+                        success = false;
+                    }
+                    break;
+                case "no_nonce":
+                    // server returns a wrong WWW-Authenticate header (no nonce)
+                    server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_NONCE);
+                    success = testAuth(url, auth, EXPECT_FAILURE);
+                    break;
+                case "invalid_alg":
+                    // server returns a wrong WWW-Authenticate header
+                    // (invalid hash algorithm)
+                    server.setWWWAuthHeader(WWW_AUTH_HEADER_INVALID_ALGORITHM);
+                    success = testAuth(url, auth, EXPECT_FAILURE);
+                    break;
+                case "no_qop":
+                    // server returns a good WWW-Authenticate header
+                    // without QOPs
+                    server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_QOP);
+                    success = testAuth(url, auth, EXPECT_DIGEST);
+                    break;
+                default:
+                    throw new RuntimeException("Unexpected testcase: "
+                            + testcase);
+            }
+
+            if (!success) {
+                throw new RuntimeException("Test failed");
+            }
+        }
+
+        System.out.println("Test passed");
+    }
+
+    static boolean testAuth(String url, AuthenticatorImpl auth,
+            String expectedScheme) {
+
+        try {
+            System.out.printf("Connect to %s, expected auth scheme is '%s'%n",
+                    url, expectedScheme);
+            load(url);
+
+            if (expectedScheme == null) {
+                System.out.println("Unexpected successful connection");
+                return false;
+            }
+
+            System.out.printf("Actual auth scheme is '%s'%n",
+                    auth.lastRequestedScheme);
+            if (!expectedScheme.equalsIgnoreCase(auth.lastRequestedScheme)) {
+                System.out.println("Unexpected auth scheme");
+                return false;
+            }
+        } catch (IOException e) {
+            if (expectedScheme != null) {
+                System.out.println("Unexpected exception: " + e);
+                e.printStackTrace(System.out);
+                return false;
+            }
+            System.out.println("Expected exception: " + e);
+        }
+
+        return true;
+    }
+
+    static void load(String url) throws IOException {
+        URLConnection conn = new URL(url).openConnection();
+        conn.setUseCaches(false);
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(conn.getInputStream()))) {
+
+            String line = reader.readLine();
+            if (line == null) {
+                throw new IOException("Couldn't read response");
+            }
+            do {
+                System.out.println(line);
+            } while ((line = reader.readLine()) != null);
+        }
+    }
+
+    private static class AuthenticatorImpl extends Authenticator {
+
+        private String lastRequestedScheme;
+        private String lastRequestedPrompt;
+
+        @Override
+        public PasswordAuthentication getPasswordAuthentication() {
+            lastRequestedScheme = getRequestingScheme();
+            lastRequestedPrompt = getRequestingPrompt();
+            System.out.println("AuthenticatorImpl: requested "
+                    + lastRequestedScheme);
+
+            return new PasswordAuthentication("Mufasa",
+                    "Circle Of Life".toCharArray());
+        }
+    }
+
+    // local HTTP server which pretends to support HTTP Digest auth
+    static class LocalHttpServer implements HttpHandler, AutoCloseable {
+
+        private final HttpServer server;
+        private volatile String wwwAuthHeader = null;
+        private volatile String authInfoHeader = null;
+        private volatile String lastRequestedNonce;
+
+        private LocalHttpServer(HttpServer server) {
+            this.server = server;
+        }
+
+        void setWWWAuthHeader(String wwwAuthHeader) {
+            this.wwwAuthHeader = wwwAuthHeader;
+        }
+
+        void setAuthInfoHeader(String authInfoHeader) {
+            this.authInfoHeader = authInfoHeader;
+        }
+
+        static LocalHttpServer startServer() throws IOException {
+            HttpServer httpServer = HttpServer.create(
+                    new InetSocketAddress(0), 0);
+            LocalHttpServer localHttpServer = new LocalHttpServer(httpServer);
+            localHttpServer.start();
+
+            return localHttpServer;
+        }
+
+        void start() {
+            server.createContext("/test", this);
+            server.start();
+            System.out.println("HttpServer: started on port " + getPort());
+        }
+
+        void stop() {
+            server.stop(0);
+            System.out.println("HttpServer: stopped");
+        }
+
+        int getPort() {
+            return server.getAddress().getPort();
+        }
+
+        @Override
+        public void handle(HttpExchange t) throws IOException {
+            System.out.println("HttpServer: handle connection");
+
+            // read a request
+            try (InputStream is = t.getRequestBody()) {
+                while (is.read() > 0);
+            }
+
+            try {
+                List<String> headers = t.getRequestHeaders()
+                        .get("Authorization");
+                String header = "";
+                if (headers != null && !headers.isEmpty()) {
+                    header = headers.get(0).trim().toLowerCase();
+                }
+                if (header.startsWith("digest")) {
+                    if (authInfoHeader != null) {
+                        t.getResponseHeaders().add("Authentication-Info",
+                                authInfoHeader);
+                    }
+                    lastRequestedNonce = findParameter(header, "nonce");
+                    byte[] output = "hello".getBytes();
+                    t.sendResponseHeaders(200, output.length);
+                    t.getResponseBody().write(output);
+                    System.out.println("HttpServer: return 200");
+                } else {
+                    if (wwwAuthHeader != null) {
+                        t.getResponseHeaders().add(
+                                "WWW-Authenticate", wwwAuthHeader);
+                    }
+                    byte[] output = "forbidden".getBytes();
+                    t.sendResponseHeaders(401, output.length);
+                    t.getResponseBody().write(output);
+                    System.out.println("HttpServer: return 401");
+                }
+            } catch (IOException e) {
+                System.out.println("HttpServer: exception: " + e);
+                System.out.println("HttpServer: return 500");
+                t.sendResponseHeaders(500, 0);
+            } finally {
+                t.close();
+            }
+        }
+
+        private static String findParameter(String header, String name) {
+            name = name.toLowerCase();
+            if (header != null) {
+                String[] params = header.split("\\s");
+                for (String param : params) {
+                    param = param.trim().toLowerCase();
+                    if (param.startsWith(name)) {
+                        String[] parts = param.split("=");
+                        if (parts.length > 1) {
+                            return parts[1]
+                                    .replaceAll("\"", "").replaceAll(",", "");
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public void close() {
+            stop();
+        }
+    }
+}