# HG changeset patch # User asmotrak # Date 1453396531 28800 # Node ID 74feccca64ccb8029dabfb1d60f818c6bb8a6918 # Parent 500c5fc13aa4f9266eab9c82def7437ae33714d9 8138990: Implementation of HTTP Digest authentication may be more flexible Reviewed-by: michaelm diff -r 500c5fc13aa4 -r 74feccca64cc jdk/src/java.base/share/classes/sun/net/www/protocol/http/DigestAuthentication.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; diff -r 500c5fc13aa4 -r 74feccca64cc jdk/test/sun/net/www/http/HttpURLConnection/DigestAuth.java --- /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 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(); + } + } +}