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