DigestAuth.java revision 14606:bc3775e25b52
1/* 2 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24import com.sun.net.httpserver.HttpExchange; 25import com.sun.net.httpserver.HttpHandler; 26import com.sun.net.httpserver.HttpServer; 27import java.io.BufferedReader; 28import java.io.InputStreamReader; 29import java.io.IOException; 30import java.io.InputStream; 31import java.net.Authenticator; 32import java.net.InetSocketAddress; 33import java.net.PasswordAuthentication; 34import java.net.URL; 35import java.net.URLConnection; 36import java.util.List; 37 38/* 39 * @test 40 * @bug 8138990 41 * @summary Tests for HTTP Digest auth 42 * The impl maintains a cache for auth info, 43 * the testcases run in a separate JVM to avoid cache hits 44 * @modules jdk.httpserver 45 * @run main/othervm DigestAuth good 46 * @run main/othervm DigestAuth only_nonce 47 * @run main/othervm DigestAuth sha1 48 * @run main/othervm DigestAuth no_header 49 * @run main/othervm DigestAuth no_nonce 50 * @run main/othervm DigestAuth no_qop 51 * @run main/othervm DigestAuth invalid_alg 52 * @run main/othervm DigestAuth validate_server 53 * @run main/othervm DigestAuth validate_server_no_qop 54 */ 55public class DigestAuth { 56 57 static final String LOCALHOST = "localhost"; 58 static final String EXPECT_FAILURE = null; 59 static final String EXPECT_DIGEST = "Digest"; 60 static final String REALM = "testrealm@host.com"; 61 static final String NEXT_NONCE = "40f2e879449675f288476d772627370a"; 62 63 static final String GOOD_WWW_AUTH_HEADER = "Digest " 64 + "realm=\"testrealm@host.com\", " 65 + "qop=\"auth,auth-int\", " 66 + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " 67 + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; 68 69 static final String GOOD_WWW_AUTH_HEADER_NO_QOP = "Digest " 70 + "realm=\"testrealm@host.com\", " 71 + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " 72 + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; 73 74 static final String WWW_AUTH_HEADER_NO_NONCE = "Digest " 75 + "realm=\"testrealm@host.com\", " 76 + "qop=\"auth,auth-int\", " 77 + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; 78 79 static final String WWW_AUTH_HEADER_NO_QOP = "Digest " 80 + "realm=\"testrealm@host.com\", " 81 + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " 82 + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; 83 84 static final String WWW_AUTH_HEADER_ONLY_NONCE = "Digest " 85 + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""; 86 87 static final String WWW_AUTH_HEADER_SHA1 = "Digest " 88 + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " 89 + "algorithm=\"SHA1\""; 90 91 static final String WWW_AUTH_HEADER_INVALID_ALGORITHM = "Digest " 92 + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " 93 + "algorithm=\"SHA123\""; 94 95 static final String AUTH_INFO_HEADER_NO_QOP_FIRST = 96 "nextnonce=\"" + NEXT_NONCE + "\", " 97 + "rspauth=\"ee85bc4315d8b18757809f1a8b9382d8\""; 98 99 static final String AUTH_INFO_HEADER_NO_QOP_SECOND = 100 "rspauth=\"12f2fa12841b3775b6054576722446b2\""; 101 102 static final String AUTH_INFO_HEADER_WRONG_DIGEST = 103 "nextnonce=\"" + NEXT_NONCE + "\", " 104 + "rspauth=\"7327570c586207eca2afae94fc20903d\", " 105 + "cnonce=\"0a4f113b\", " 106 + "nc=00000001, " 107 + "qop=auth"; 108 109 public static void main(String[] args) throws Exception { 110 if (args.length == 0) { 111 throw new RuntimeException("No testcase specified"); 112 } 113 String testcase = args[0]; 114 115 // start a local HTTP server 116 try (LocalHttpServer server = LocalHttpServer.startServer()) { 117 118 // set authenticator 119 AuthenticatorImpl auth = new AuthenticatorImpl(); 120 Authenticator.setDefault(auth); 121 122 String url = String.format("http://%s:%d/test/", 123 LOCALHOST, server.getPort()); 124 125 boolean success = true; 126 switch (testcase) { 127 case "good": 128 // server returns a good WWW-Authenticate header 129 server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER); 130 success = testAuth(url, auth, EXPECT_DIGEST); 131 if (auth.lastRequestedPrompt == null || 132 !auth.lastRequestedPrompt.equals(REALM)) { 133 System.out.println("Unexpected realm: " 134 + auth.lastRequestedPrompt); 135 success = false; 136 } 137 break; 138 case "validate_server": 139 // enable processing Authentication-Info headers 140 System.setProperty("http.auth.digest.validateServer", 141 "true"); 142 143 /* Server returns good WWW-Authenticate 144 * and Authentication-Info headers with wrong digest 145 */ 146 server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER); 147 server.setAuthInfoHeader(AUTH_INFO_HEADER_WRONG_DIGEST); 148 success = testAuth(url, auth, EXPECT_FAILURE); 149 if (auth.lastRequestedPrompt == null || 150 !auth.lastRequestedPrompt.equals(REALM)) { 151 System.out.println("Unexpected realm: " 152 + auth.lastRequestedPrompt); 153 success = false; 154 } 155 break; 156 case "validate_server_no_qop": 157 // enable processing Authentication-Info headers 158 System.setProperty("http.auth.digest.validateServer", 159 "true"); 160 161 /* Server returns good both WWW-Authenticate 162 * and Authentication-Info headers without any qop field, 163 * so that client-nonce should not be taked into account, 164 * and connection should succeed. 165 */ 166 server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER_NO_QOP); 167 server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_FIRST); 168 success = testAuth(url, auth, EXPECT_DIGEST); 169 if (auth.lastRequestedPrompt == null || 170 !auth.lastRequestedPrompt.equals(REALM)) { 171 System.out.println("Unexpected realm: " 172 + auth.lastRequestedPrompt); 173 success = false; 174 } 175 176 // connect again and check if nextnonce was used 177 server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_SECOND); 178 success &= testAuth(url, auth, EXPECT_DIGEST); 179 if (!NEXT_NONCE.equals(server.lastRequestedNonce)) { 180 System.out.println("Unexpected next nonce: " 181 + server.lastRequestedNonce); 182 success = false; 183 } 184 break; 185 case "only_nonce": 186 /* Server returns a good WWW-Authenticate header 187 * which contains only nonce (no realm set). 188 * 189 * Realm from WWW-Authenticate header is passed to 190 * authenticator which can use it as a prompt 191 * when it asks a user for credentials. 192 * 193 * It's fine if an HTTP client doesn't fail if no realm set, 194 * and delegates making a decision to authenticator/user. 195 */ 196 server.setWWWAuthHeader(WWW_AUTH_HEADER_ONLY_NONCE); 197 success = testAuth(url, auth, EXPECT_DIGEST); 198 if (auth.lastRequestedPrompt != null && 199 !auth.lastRequestedPrompt.trim().isEmpty()) { 200 System.out.println("Unexpected realm: " 201 + auth.lastRequestedPrompt); 202 success = false; 203 } 204 break; 205 case "sha1": 206 // server returns a good WWW-Authenticate header with SHA-1 207 server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1); 208 success = testAuth(url, auth, EXPECT_DIGEST); 209 break; 210 case "no_header": 211 // server returns no WWW-Authenticate header 212 success = testAuth(url, auth, EXPECT_FAILURE); 213 if (auth.lastRequestedScheme != null) { 214 System.out.println("Unexpected scheme: " 215 + auth.lastRequestedScheme); 216 success = false; 217 } 218 break; 219 case "no_nonce": 220 // server returns a wrong WWW-Authenticate header (no nonce) 221 server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_NONCE); 222 success = testAuth(url, auth, EXPECT_FAILURE); 223 break; 224 case "invalid_alg": 225 // server returns a wrong WWW-Authenticate header 226 // (invalid hash algorithm) 227 server.setWWWAuthHeader(WWW_AUTH_HEADER_INVALID_ALGORITHM); 228 success = testAuth(url, auth, EXPECT_FAILURE); 229 break; 230 case "no_qop": 231 // server returns a good WWW-Authenticate header 232 // without QOPs 233 server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_QOP); 234 success = testAuth(url, auth, EXPECT_DIGEST); 235 break; 236 default: 237 throw new RuntimeException("Unexpected testcase: " 238 + testcase); 239 } 240 241 if (!success) { 242 throw new RuntimeException("Test failed"); 243 } 244 } 245 246 System.out.println("Test passed"); 247 } 248 249 static boolean testAuth(String url, AuthenticatorImpl auth, 250 String expectedScheme) { 251 252 try { 253 System.out.printf("Connect to %s, expected auth scheme is '%s'%n", 254 url, expectedScheme); 255 load(url); 256 257 if (expectedScheme == null) { 258 System.out.println("Unexpected successful connection"); 259 return false; 260 } 261 262 System.out.printf("Actual auth scheme is '%s'%n", 263 auth.lastRequestedScheme); 264 if (!expectedScheme.equalsIgnoreCase(auth.lastRequestedScheme)) { 265 System.out.println("Unexpected auth scheme"); 266 return false; 267 } 268 } catch (IOException e) { 269 if (expectedScheme != null) { 270 System.out.println("Unexpected exception: " + e); 271 e.printStackTrace(System.out); 272 return false; 273 } 274 System.out.println("Expected exception: " + e); 275 } 276 277 return true; 278 } 279 280 static void load(String url) throws IOException { 281 URLConnection conn = new URL(url).openConnection(); 282 conn.setUseCaches(false); 283 try (BufferedReader reader = new BufferedReader( 284 new InputStreamReader(conn.getInputStream()))) { 285 286 String line = reader.readLine(); 287 if (line == null) { 288 throw new IOException("Couldn't read response"); 289 } 290 do { 291 System.out.println(line); 292 } while ((line = reader.readLine()) != null); 293 } 294 } 295 296 private static class AuthenticatorImpl extends Authenticator { 297 298 private String lastRequestedScheme; 299 private String lastRequestedPrompt; 300 301 @Override 302 public PasswordAuthentication getPasswordAuthentication() { 303 lastRequestedScheme = getRequestingScheme(); 304 lastRequestedPrompt = getRequestingPrompt(); 305 System.out.println("AuthenticatorImpl: requested " 306 + lastRequestedScheme); 307 308 return new PasswordAuthentication("Mufasa", 309 "Circle Of Life".toCharArray()); 310 } 311 } 312 313 // local HTTP server which pretends to support HTTP Digest auth 314 static class LocalHttpServer implements HttpHandler, AutoCloseable { 315 316 private final HttpServer server; 317 private volatile String wwwAuthHeader = null; 318 private volatile String authInfoHeader = null; 319 private volatile String lastRequestedNonce; 320 321 private LocalHttpServer(HttpServer server) { 322 this.server = server; 323 } 324 325 void setWWWAuthHeader(String wwwAuthHeader) { 326 this.wwwAuthHeader = wwwAuthHeader; 327 } 328 329 void setAuthInfoHeader(String authInfoHeader) { 330 this.authInfoHeader = authInfoHeader; 331 } 332 333 static LocalHttpServer startServer() throws IOException { 334 HttpServer httpServer = HttpServer.create( 335 new InetSocketAddress(0), 0); 336 LocalHttpServer localHttpServer = new LocalHttpServer(httpServer); 337 localHttpServer.start(); 338 339 return localHttpServer; 340 } 341 342 void start() { 343 server.createContext("/test", this); 344 server.start(); 345 System.out.println("HttpServer: started on port " + getPort()); 346 } 347 348 void stop() { 349 server.stop(0); 350 System.out.println("HttpServer: stopped"); 351 } 352 353 int getPort() { 354 return server.getAddress().getPort(); 355 } 356 357 @Override 358 public void handle(HttpExchange t) throws IOException { 359 System.out.println("HttpServer: handle connection"); 360 361 // read a request 362 try (InputStream is = t.getRequestBody()) { 363 while (is.read() > 0); 364 } 365 366 try { 367 List<String> headers = t.getRequestHeaders() 368 .get("Authorization"); 369 String header = ""; 370 if (headers != null && !headers.isEmpty()) { 371 header = headers.get(0).trim().toLowerCase(); 372 } 373 if (header.startsWith("digest")) { 374 if (authInfoHeader != null) { 375 t.getResponseHeaders().add("Authentication-Info", 376 authInfoHeader); 377 } 378 lastRequestedNonce = findParameter(header, "nonce"); 379 byte[] output = "hello".getBytes(); 380 t.sendResponseHeaders(200, output.length); 381 t.getResponseBody().write(output); 382 System.out.println("HttpServer: return 200"); 383 } else { 384 if (wwwAuthHeader != null) { 385 t.getResponseHeaders().add( 386 "WWW-Authenticate", wwwAuthHeader); 387 } 388 byte[] output = "forbidden".getBytes(); 389 t.sendResponseHeaders(401, output.length); 390 t.getResponseBody().write(output); 391 System.out.println("HttpServer: return 401"); 392 } 393 } catch (IOException e) { 394 System.out.println("HttpServer: exception: " + e); 395 System.out.println("HttpServer: return 500"); 396 t.sendResponseHeaders(500, 0); 397 } finally { 398 t.close(); 399 } 400 } 401 402 private static String findParameter(String header, String name) { 403 name = name.toLowerCase(); 404 if (header != null) { 405 String[] params = header.split("\\s"); 406 for (String param : params) { 407 param = param.trim().toLowerCase(); 408 if (param.startsWith(name)) { 409 String[] parts = param.split("="); 410 if (parts.length > 1) { 411 return parts[1] 412 .replaceAll("\"", "").replaceAll(",", ""); 413 } 414 } 415 } 416 } 417 return null; 418 } 419 420 @Override 421 public void close() { 422 stop(); 423 } 424 } 425} 426