/* * Copyright (c) 2016, 2022, 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.InetAddress; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLConnection; import java.net.HttpURLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.Level; import static java.util.Map.entry; /* * @test * @bug 8138990 8281561 * @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 * @modules jdk.httpserver * @run main/othervm DigestAuth bad * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth good * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth only_nonce * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=SHA-1 DigestAuth sha1-good * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth sha1-bad * @run main/othervm DigestAuth sha256 * @run main/othervm DigestAuth sha512 * @run main/othervm DigestAuth sha256-userhash * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth sha256 * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth no_header * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth no_nonce * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth no_qop * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth invalid_alg * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth validate_server * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth validate_server_no_qop */ /* * The sha512-256-userhash case must be run manually. It needs to run with sudo as the * test must bind to port 80. You also need a modified JDK where * sun.net.www.protocol.http.DigestAuthentication.getCnonce * returns the hardcoded cnonce value below (normally it is chosen at random) * "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v" * It can be run from the command line directly as follows: * sudo java -Djdk.net.hosts.file=hosts DigestAuth sha512-256-userhash port80 * assuming you are running in the test source directory */ public class DigestAuth { 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_SHA256 = "Digest " + "nonce=\"a69ae8a2e17c219bc6c118b673e93601616a6a" + "4d8fde3a19996748d77ad0464b\", qop=\"auth\", " + "opaque=\"efc62777cff802cb29252f626b041f381cd360" + "7187115871ca25e7b51a3757e9\", algorithm=SHA-256"; static final String WWW_AUTH_HEADER_SHA512 = "Digest " + "nonce=\"9aaa8d3ae53b54ce653a5d52d895afcd9c0e430" + "a17bdf98bb34235af84fba268d31376a63e0c39079b519" + "c14baa0429754266f35b62a47b9c8b5d3d36c638282\"," + " qop=\"auth\", opaque=\"28cdc6bae6c5dd7ec89dbf" + "af4d4f26b70f41ebbb83dc7af0950d6de016c40f412224" + "676cd45ebcf889a70e65a2b055a8b5232e50281272ba7c" + "67628cc3bb3492\", algorithm=SHA-512"; static final String WWW_AUTH_HEADER_SHA_256_UHASH = "Digest " + "realm=\"testrealm@host.com\", " + "qop=\"auth\", algorithm=SHA-256," + "nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC" + "/RVvkK\", opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGP" + "ChXYjwrI2QmXDnsOS\", charset=UTF-8, userhash=true"; 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"; // These two must be run manually with a modified JDK // that generates the exact cnonce given below. static final String SHA_512_256_FIRST = "Digest " + "realm=\"api@example.org\", " + "qop=\"auth\", " + "algorithm=SHA-512-256, " + "nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", " + "opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", " + "charset=UTF-8, " + "userhash=true "; // Below taken from corrected version of RFC 7616 static final Map<String,String> SHA_512_256_EXPECTED = Map.ofEntries( entry("username", "793263caabb707a56211940d90411ea4a575adeccb" + "7e360aeb624ed06ece9b0b"), entry("realm", "api@example.org"), entry("uri", "/doe.json"), entry("algorithm", "SHA-512-256"), entry("nonce", "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"), entry("nc", "00000001"), entry("cnonce", "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"), entry("qop", "auth"), entry("response", "3798d4131c277846293534c3edc11bd8a5e4cdcbff78" + "b05db9d95eeb1cec68a5"), entry("opaque", "HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS"), entry("userhash", "true")); public static void main(String[] args) throws Exception { if (args.length == 0) { throw new RuntimeException("No testcase specified"); } String testcase = args[0]; System.out.println("Running test: " + testcase); boolean usePort80 = args.length > 1 && args[1].equals("port80"); // start a local HTTP server try (LocalHttpServer server = LocalHttpServer.startServer(usePort80)) { // set authenticator AuthenticatorImpl auth = new AuthenticatorImpl(); String url = String.format("http://%s/test/", server.getAuthority()); boolean success = true; switch (testcase) { case "sha512-256-userhash": auth = new AuthenticatorImpl("J\u00e4s\u00f8n Doe", "Secret, or not?"); // file based name service must be used so domain // below resolves to localhost if (usePort80) { url = "http://api.example.org/doe.json"; } else { url = "http://api.example.org:" + server.getPort() + "/doe.json"; } server.setWWWAuthHeader(SHA_512_256_FIRST); server.setExpectedRequestParams(SHA_512_256_EXPECTED); success = testAuth(url, auth, EXPECT_DIGEST); break; case "bad": // server returns a good WWW-Authenticate header with MD5 // but MD5 is disallowed by default server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER); 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 "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-good": // server returns a good WWW-Authenticate header with SHA-1 server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1); success = testAuth(url, auth, EXPECT_DIGEST); break; case "sha1-bad": // server returns a WWW-Authenticate header with SHA-1 // but SHA-1 disabled server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1); success = testAuth(url, auth, EXPECT_FAILURE); break; case "sha256": // server returns a good WWW-Authenticate header with SHA-256 server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA256); success = testAuth(url, auth, EXPECT_DIGEST); break; case "sha512": // server returns a good WWW-Authenticate header with SHA-512 server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA512); success = testAuth(url, auth, EXPECT_DIGEST); break; case "sha256-userhash": // server returns a good WWW-Authenticate header with SHA-256 // also sets the userhash=true parameter server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA_256_UHASH); success = testAuth(url, auth, EXPECT_DIGEST); // make sure the userhash parameter was set correctly // and the username itself is the correct hash server.checkUserHash(getUserHash("SHA-256", "Mufasa", REALM)); 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, auth); 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, Authenticator auth) throws IOException { HttpURLConnection conn = (HttpURLConnection)(new URL(url).openConnection()); conn.setAuthenticator(auth); 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); } } public static String getUserHash(String alg, String user, String realm) { try { MessageDigest md = MessageDigest.getInstance(alg); String msg = user + ":" + realm; //String msg = "Mufasa:testrealm@host.com"; byte[] output = md.digest(msg.getBytes(StandardCharsets.ISO_8859_1)); StringBuilder sb = new StringBuilder(); for (int i=0; i<output.length; i++) { String s1 = Integer.toHexString(output[i] & 0xf); String s2 = Integer.toHexString(Byte.toUnsignedInt(output[i]) >>> 4); sb.append(s2).append(s1); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } private static class AuthenticatorImpl extends Authenticator { private String lastRequestedScheme; private String lastRequestedPrompt; private final String user, pass; AuthenticatorImpl() { this("Mufasa", "Circle Of Life"); } AuthenticatorImpl(String user, String pass) { this.user = user; this.pass = pass; } @Override public PasswordAuthentication getPasswordAuthentication() { lastRequestedScheme = getRequestingScheme(); lastRequestedPrompt = getRequestingPrompt(); System.out.println("AuthenticatorImpl: requested " + lastRequestedScheme); return new PasswordAuthentication(user, pass.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 volatile String lastRequestedUser; private volatile String lastRequestedUserhash; private volatile Map<String,String> expectedParams; private LocalHttpServer(HttpServer server) { this.server = server; } public String getAuthority() { InetAddress address = server.getAddress().getAddress(); String hostaddr = address.isAnyLocalAddress() ? "localhost" : address.getHostAddress(); if (hostaddr.indexOf(':') > -1) { hostaddr = "[" + hostaddr + "]"; } return hostaddr + ":" + getPort(); } void setWWWAuthHeader(String wwwAuthHeader) { this.wwwAuthHeader = wwwAuthHeader; } void setExpectedRequestParams(Map<String,String> params) { this.expectedParams = params; } void setAuthInfoHeader(String authInfoHeader) { this.authInfoHeader = authInfoHeader; } void checkUserHash(String expectedUser) { boolean pass = true; if (!expectedUser.equals(lastRequestedUser)) { System.out.println("Username mismatch:"); System.out.println("Expected: " + expectedUser); System.out.println("Received: " + lastRequestedUser); pass = false; } if (!lastRequestedUserhash.equalsIgnoreCase("true")) { System.out.println("Userhash mismatch:"); pass = false; } if (!pass) { throw new RuntimeException("Test failed: checkUserHash"); } } void checkExpectedParams(String header) { if (expectedParams == null) return; expectedParams.forEach((name, value) -> { String rxValue = findParameter(header, name); if (!rxValue.equalsIgnoreCase(value)) { throw new RuntimeException("value mismatch " + "name = " + name + " (" + rxValue + "/" + value + ")"); } }); } static LocalHttpServer startServer(boolean usePort80) throws IOException { int port = usePort80 ? 80 : 0; InetAddress loopback = InetAddress.getLoopbackAddress(); HttpServer httpServer = HttpServer.create( new InetSocketAddress(loopback, port), 0); LocalHttpServer localHttpServer = new LocalHttpServer(httpServer); localHttpServer.start(); return localHttpServer; } void start() { server.createContext("/test", this); server.createContext("/", this); server.start(); System.out.println("HttpServer: started on port " + getAuthority()); } 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); } checkExpectedParams(header); lastRequestedNonce = findParameter(header, "nonce"); lastRequestedUser = findParameter(header, "username"); lastRequestedUserhash = findParameter(header, "userhash"); 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(); } } }