1 /* 2 * Copyright (c) 2009, 2018, 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 24 /* 25 * @test 26 * @bug 6578647 6829283 8171340 8194486 27 * @modules java.base/sun.security.util 28 * java.security.jgss/sun.security.krb5.internal:+open 29 * java.security.jgss/sun.security.jgss 30 * java.security.jgss/sun.security.krb5:+open 31 * java.security.jgss/sun.security.jgss.krb5 32 * java.security.jgss/sun.security.krb5.internal.ccache 33 * java.security.jgss/sun.security.krb5.internal.crypto 34 * java.security.jgss/sun.security.krb5.internal.ktab 35 * jdk.security.auth 36 * jdk.security.jgss 37 * jdk.httpserver 38 * @summary Undefined requesting URL in java.net.Authenticator 39 * .getPasswordAuthentication() 40 * @summary HTTP/Negotiate: Authenticator triggered again when 41 * user cancels the first one 42 * @library /test/lib 43 * @run main jdk.test.lib.FileInstaller TestHosts TestHosts 44 * @run main/othervm -Djdk.net.hosts.file=TestHosts HttpNegotiateServer 45 */ 46 47 import com.sun.net.httpserver.Headers; 48 import com.sun.net.httpserver.HttpContext; 49 import com.sun.net.httpserver.HttpExchange; 50 import com.sun.net.httpserver.HttpHandler; 51 import com.sun.net.httpserver.HttpServer; 52 import com.sun.net.httpserver.HttpPrincipal; 53 import com.sun.security.auth.module.Krb5LoginModule; 54 import java.io.BufferedReader; 55 import java.io.File; 56 import java.io.FileOutputStream; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.InputStreamReader; 60 import java.net.HttpURLConnection; 61 import java.net.InetSocketAddress; 62 import java.net.PasswordAuthentication; 63 import java.net.Proxy; 64 import java.net.URL; 65 import java.net.URLConnection; 66 import java.security.*; 67 import java.util.HashMap; 68 import java.util.Map; 69 import javax.security.auth.Subject; 70 import javax.security.auth.callback.Callback; 71 import javax.security.auth.callback.CallbackHandler; 72 import javax.security.auth.callback.NameCallback; 73 import javax.security.auth.callback.PasswordCallback; 74 import javax.security.auth.callback.UnsupportedCallbackException; 75 import javax.security.auth.login.AppConfigurationEntry; 76 import javax.security.auth.login.Configuration; 77 import javax.security.auth.login.LoginContext; 78 import javax.security.auth.login.LoginException; 79 import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; 80 import org.ietf.jgss.GSSContext; 81 import org.ietf.jgss.GSSCredential; 82 import org.ietf.jgss.GSSManager; 83 import sun.security.jgss.GSSUtil; 84 import sun.security.krb5.Config; 85 import java.util.Base64; 86 87 /** 88 * Basic JGSS/krb5 test with 3 parties: client, server, backend server. Each 89 * party uses JAAS login to get subjects and executes JGSS calls using 90 * Subject.doAs. 91 */ 92 public class HttpNegotiateServer { 93 94 // Two realm, web server in one, proxy server in another 95 final static String REALM_WEB = "WEB.DOMAIN"; 96 final static String REALM_PROXY = "PROXY.DOMAIN"; 97 final static String KRB5_CONF = "web.conf"; 98 final static String KRB5_TAB = "web.ktab"; 99 100 // user principals 101 final static String WEB_USER = "web"; 102 final static char[] WEB_PASS = "webby".toCharArray(); 103 final static String PROXY_USER = "pro"; 104 final static char[] PROXY_PASS = "proxy".toCharArray(); 105 106 107 final static String WEB_HOST = "host.web.domain"; 108 final static String PROXY_HOST = "host.proxy.domain"; 109 110 // web page content 111 final static String CONTENT = "Hello, World!"; 112 113 // For 6829283, count how many times the Authenticator is called. 114 static int count = 0; 115 116 static int webPort, proxyPort; 117 118 // URLs for web test, proxy test. The proxy server is not a real proxy 119 // since it fakes the same content for any URL. :) 120 static URL webUrl, proxyUrl; 121 122 /** 123 * This Authenticator checks everything: 124 * scheme, protocol, requestor type, host, port, and url 125 */ 126 static class KnowAllAuthenticator extends java.net.Authenticator { getPasswordAuthentication()127 public PasswordAuthentication getPasswordAuthentication () { 128 if (!getRequestingScheme().equalsIgnoreCase("Negotiate")) { 129 throw new RuntimeException("Bad scheme"); 130 } 131 if (!getRequestingProtocol().equalsIgnoreCase("HTTP")) { 132 throw new RuntimeException("Bad protocol"); 133 } 134 if (getRequestorType() == RequestorType.SERVER) { 135 if (!this.getRequestingHost().equalsIgnoreCase(webUrl.getHost())) { 136 throw new RuntimeException("Bad host"); 137 } 138 if (this.getRequestingPort() != webUrl.getPort()) { 139 throw new RuntimeException("Bad port"); 140 } 141 if (!this.getRequestingURL().equals(webUrl)) { 142 throw new RuntimeException("Bad url"); 143 } 144 return new PasswordAuthentication( 145 WEB_USER+"@"+REALM_WEB, WEB_PASS); 146 } else if (getRequestorType() == RequestorType.PROXY) { 147 if (!this.getRequestingHost().equalsIgnoreCase(PROXY_HOST)) { 148 throw new RuntimeException("Bad host"); 149 } 150 if (this.getRequestingPort() != proxyPort) { 151 throw new RuntimeException("Bad port"); 152 } 153 if (!this.getRequestingURL().equals(proxyUrl)) { 154 throw new RuntimeException("Bad url"); 155 } 156 return new PasswordAuthentication( 157 PROXY_USER+"@"+REALM_PROXY, PROXY_PASS); 158 } else { 159 throw new RuntimeException("Bad requster type"); 160 } 161 } 162 } 163 164 /** 165 * This Authenticator knows nothing 166 */ 167 static class KnowNothingAuthenticator extends java.net.Authenticator { 168 @Override getPasswordAuthentication()169 public PasswordAuthentication getPasswordAuthentication () { 170 HttpNegotiateServer.count++; 171 return null; 172 } 173 } 174 main(String[] args)175 public static void main(String[] args) 176 throws Exception { 177 178 System.setProperty("sun.security.krb5.debug", "true"); 179 180 KDC kdcw = KDC.create(REALM_WEB); 181 kdcw.addPrincipal(WEB_USER, WEB_PASS); 182 kdcw.addPrincipalRandKey("krbtgt/" + REALM_WEB); 183 kdcw.addPrincipalRandKey("HTTP/" + WEB_HOST); 184 185 KDC kdcp = KDC.create(REALM_PROXY); 186 kdcp.addPrincipal(PROXY_USER, PROXY_PASS); 187 kdcp.addPrincipalRandKey("krbtgt/" + REALM_PROXY); 188 kdcp.addPrincipalRandKey("HTTP/" + PROXY_HOST); 189 190 KDC.saveConfig(KRB5_CONF, kdcw, kdcp, 191 "default_keytab_name = " + KRB5_TAB, 192 "[domain_realm]", 193 "", 194 ".web.domain="+REALM_WEB, 195 ".proxy.domain="+REALM_PROXY); 196 197 System.setProperty("java.security.krb5.conf", KRB5_CONF); 198 Config.refresh(); 199 KDC.writeMultiKtab(KRB5_TAB, kdcw, kdcp); 200 201 // Write a customized JAAS conf file, so that any kinit cache 202 // will be ignored. 203 System.setProperty("java.security.auth.login.config", OneKDC.JAAS_CONF); 204 File f = new File(OneKDC.JAAS_CONF); 205 FileOutputStream fos = new FileOutputStream(f); 206 fos.write(( 207 "com.sun.security.jgss.krb5.initiate {\n" + 208 " com.sun.security.auth.module.Krb5LoginModule required;\n};\n" 209 ).getBytes()); 210 fos.close(); 211 212 HttpServer h1 = httpd("Negotiate", false, 213 "HTTP/" + WEB_HOST + "@" + REALM_WEB, KRB5_TAB); 214 webPort = h1.getAddress().getPort(); 215 HttpServer h2 = httpd("Negotiate", true, 216 "HTTP/" + PROXY_HOST + "@" + REALM_PROXY, KRB5_TAB); 217 proxyPort = h2.getAddress().getPort(); 218 219 webUrl = new URL("http://" + WEB_HOST +":" + webPort + "/a/b/c"); 220 proxyUrl = new URL("http://nosuchplace/a/b/c"); 221 222 try { 223 Exception e1 = null, e2 = null, e3 = null; 224 try { 225 test6578647(); 226 } catch (Exception e) { 227 e1 = e; 228 e.printStackTrace(); 229 } 230 try { 231 test6829283(); 232 } catch (Exception e) { 233 e2 = e; 234 e.printStackTrace(); 235 } 236 try { 237 test8077155(); 238 } catch (Exception e) { 239 e3 = e; 240 e.printStackTrace(); 241 } 242 243 if (e1 != null || e2 != null || e3 != null) { 244 throw new RuntimeException("Test error"); 245 } 246 } finally { 247 // Must stop. Seems there's no HttpServer.startAsDaemon() 248 if (h1 != null) h1.stop(0); 249 if (h2 != null) h2.stop(0); 250 } 251 } 252 test6578647()253 static void test6578647() throws Exception { 254 BufferedReader reader; 255 java.net.Authenticator.setDefault(new KnowAllAuthenticator()); 256 257 reader = new BufferedReader(new InputStreamReader( 258 webUrl.openConnection(Proxy.NO_PROXY).getInputStream())); 259 if (!reader.readLine().equals(CONTENT)) { 260 throw new RuntimeException("Bad content"); 261 } 262 263 reader = new BufferedReader(new InputStreamReader( 264 proxyUrl.openConnection(new Proxy(Proxy.Type.HTTP, 265 new InetSocketAddress(PROXY_HOST, proxyPort))) 266 .getInputStream())); 267 if (!reader.readLine().equals(CONTENT)) { 268 throw new RuntimeException("Bad content"); 269 } 270 } 271 test6829283()272 static void test6829283() throws Exception { 273 BufferedReader reader; 274 java.net.Authenticator.setDefault(new KnowNothingAuthenticator()); 275 try { 276 new BufferedReader(new InputStreamReader( 277 webUrl.openConnection(Proxy.NO_PROXY).getInputStream())); 278 } catch (IOException ioe) { 279 // Will fail since no username and password is provided. 280 } 281 if (count > 1) { 282 throw new RuntimeException("Authenticator called twice"); 283 } 284 } 285 testConnect()286 static void testConnect() { 287 InputStream inputStream = null; 288 try { 289 URL url = webUrl; 290 291 URLConnection conn = url.openConnection(Proxy.NO_PROXY); 292 conn.connect(); 293 inputStream = conn.getInputStream(); 294 byte[] b = new byte[inputStream.available()]; 295 for (int j = 0; j < b.length; j++) { 296 b[j] = (byte) inputStream.read(); 297 } 298 String s = new String(b); 299 System.out.println("Length: " + s.length()); 300 System.out.println(s); 301 } catch (Exception ex) { 302 throw new RuntimeException(ex); 303 } finally { 304 if (inputStream != null) { 305 try { 306 inputStream.close(); 307 } catch (IOException e) { 308 e.printStackTrace(); 309 } 310 } 311 } 312 } 313 test8077155()314 static void test8077155() throws Exception { 315 final String username = WEB_USER; 316 final char[] password = WEB_PASS; 317 318 SecurityManager security = new SecurityManager(); 319 Policy.setPolicy(new SecurityPolicy()); 320 System.setSecurityManager(security); 321 322 CallbackHandler callback = new CallbackHandler() { 323 @Override 324 public void handle(Callback[] pCallbacks) 325 throws IOException, UnsupportedCallbackException { 326 for (Callback cb : pCallbacks) { 327 if (cb instanceof NameCallback) { 328 NameCallback ncb = (NameCallback)cb; 329 ncb.setName(username); 330 331 } else if (cb instanceof PasswordCallback) { 332 PasswordCallback pwdcb = (PasswordCallback) cb; 333 pwdcb.setPassword(password); 334 } 335 } 336 } 337 338 }; 339 340 final String jaasConfigName = "oracle.test.kerberos.login"; 341 final String krb5LoginModule 342 = "com.sun.security.auth.module.Krb5LoginModule"; 343 344 Configuration loginConfig = new Configuration() { 345 @Override 346 public AppConfigurationEntry[] getAppConfigurationEntry(String name) { 347 if (! jaasConfigName.equals(name)) { 348 return new AppConfigurationEntry[0]; 349 } 350 351 Map<String, String> options = new HashMap<String, String>(); 352 options.put("useTicketCache", Boolean.FALSE.toString()); 353 options.put("useKeyTab", Boolean.FALSE.toString()); 354 355 return new AppConfigurationEntry[] { 356 new AppConfigurationEntry(krb5LoginModule, 357 LoginModuleControlFlag.REQUIRED, 358 options) 359 }; 360 } 361 }; 362 363 // oracle context/subject/login 364 LoginContext context = null; 365 try { 366 context = new LoginContext( 367 "oracle.test.kerberos.login", null, callback, loginConfig); 368 context.login(); 369 370 } catch (LoginException ex) { 371 ex.printStackTrace(); 372 throw new RuntimeException(ex); 373 } 374 375 376 Subject subject = context.getSubject(); 377 378 final PrivilegedExceptionAction<Object> test_action 379 = new PrivilegedExceptionAction<Object>() { 380 public Object run() throws Exception { 381 testConnect(); 382 return null; 383 } 384 }; 385 386 System.err.println("\n\nExpecting to succeed when executing " + 387 "with the the logged in subject."); 388 389 try { 390 Subject.doAs(subject, test_action); 391 System.err.println("\n\nConnection succeed when executing " + 392 "with the the logged in subject."); 393 } catch (PrivilegedActionException e) { 394 System.err.println("\n\nFailure unexpected when executing " + 395 "with the the logged in subject."); 396 e.printStackTrace(); 397 throw new RuntimeException("Failed to login as subject"); 398 } 399 400 try { 401 System.err.println("\n\nExpecting to fail when running " + 402 "with the current user's login."); 403 testConnect(); 404 } catch (Exception ex) { 405 System.err.println("\nConnect failed when running " + 406 "with the current user's login:\n" + ex.getMessage()); 407 } 408 } 409 410 /** 411 * Creates and starts an HTTP or proxy server that requires 412 * Negotiate authentication. 413 * @param scheme "Negotiate" or "Kerberos" 414 * @param principal the krb5 service principal the server runs with 415 * @return the server 416 */ httpd(String scheme, boolean proxy, String principal, String ktab)417 public static HttpServer httpd(String scheme, boolean proxy, 418 String principal, String ktab) throws Exception { 419 MyHttpHandler h = new MyHttpHandler(); 420 HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); 421 HttpContext hc = server.createContext("/", h); 422 hc.setAuthenticator(new MyServerAuthenticator( 423 proxy, scheme, principal, ktab)); 424 server.start(); 425 return server; 426 } 427 428 static class MyHttpHandler implements HttpHandler { handle(HttpExchange t)429 public void handle(HttpExchange t) throws IOException { 430 t.sendResponseHeaders(200, 0); 431 t.getResponseBody().write(CONTENT.getBytes()); 432 t.close(); 433 } 434 } 435 436 static class MyServerAuthenticator 437 extends com.sun.net.httpserver.Authenticator { 438 Subject s = new Subject(); 439 GSSManager m = null; 440 GSSCredential cred = null; 441 String scheme = null; 442 String reqHdr = "WWW-Authenticate"; 443 String respHdr = "Authorization"; 444 int err = HttpURLConnection.HTTP_UNAUTHORIZED; 445 MyServerAuthenticator(boolean proxy, String scheme, String principal, String ktab)446 public MyServerAuthenticator(boolean proxy, String scheme, 447 String principal, String ktab) throws Exception { 448 449 this.scheme = scheme; 450 if (proxy) { 451 reqHdr = "Proxy-Authenticate"; 452 respHdr = "Proxy-Authorization"; 453 err = HttpURLConnection.HTTP_PROXY_AUTH; 454 } 455 456 Krb5LoginModule krb5 = new Krb5LoginModule(); 457 Map<String, String> map = new HashMap<>(); 458 Map<String, Object> shared = new HashMap<>(); 459 460 map.put("storeKey", "true"); 461 map.put("isInitiator", "false"); 462 map.put("useKeyTab", "true"); 463 map.put("keyTab", ktab); 464 map.put("principal", principal); 465 krb5.initialize(s, null, shared, map); 466 krb5.login(); 467 krb5.commit(); 468 m = GSSManager.getInstance(); 469 cred = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() { 470 @Override 471 public GSSCredential run() throws Exception { 472 System.err.println("Creating GSSCredential"); 473 return m.createCredential( 474 null, 475 GSSCredential.INDEFINITE_LIFETIME, 476 MyServerAuthenticator.this.scheme 477 .equalsIgnoreCase("Negotiate") ? 478 GSSUtil.GSS_SPNEGO_MECH_OID : 479 GSSUtil.GSS_KRB5_MECH_OID, 480 GSSCredential.ACCEPT_ONLY); 481 } 482 }); 483 } 484 485 @Override authenticate(HttpExchange exch)486 public Result authenticate(HttpExchange exch) { 487 // The GSContext is stored in an HttpContext attribute named 488 // "GSSContext" and is created at the first request. 489 GSSContext c = null; 490 String auth = exch.getRequestHeaders().getFirst(respHdr); 491 try { 492 c = (GSSContext)exch.getHttpContext() 493 .getAttributes().get("GSSContext"); 494 if (auth == null) { // First request 495 Headers map = exch.getResponseHeaders(); 496 map.set (reqHdr, scheme); // Challenge! 497 c = Subject.doAs(s, new PrivilegedExceptionAction<GSSContext>() { 498 @Override 499 public GSSContext run() throws Exception { 500 return m.createContext(cred); 501 } 502 }); 503 exch.getHttpContext().getAttributes().put("GSSContext", c); 504 return new com.sun.net.httpserver.Authenticator.Retry(err); 505 } else { // Later requests 506 byte[] token = Base64.getMimeDecoder() 507 .decode(auth.split(" ")[1]); 508 token = c.acceptSecContext(token, 0, token.length); 509 Headers map = exch.getResponseHeaders(); 510 map.set (reqHdr, scheme + " " + Base64.getMimeEncoder() 511 .encodeToString(token).replaceAll("\\s", "")); 512 if (c.isEstablished()) { 513 return new com.sun.net.httpserver.Authenticator.Success( 514 new HttpPrincipal(c.getSrcName().toString(), "")); 515 } else { 516 return new com.sun.net.httpserver.Authenticator.Retry(err); 517 } 518 } 519 } catch (Exception e) { 520 throw new RuntimeException(e); 521 } 522 } 523 } 524 } 525 526 class SecurityPolicy extends Policy { 527 528 private static Permissions perms; 529 SecurityPolicy()530 public SecurityPolicy() { 531 super(); 532 if (perms == null) { 533 perms = new Permissions(); 534 perms.add(new AllPermission()); 535 } 536 } 537 538 @Override getPermissions(CodeSource codesource)539 public PermissionCollection getPermissions(CodeSource codesource) { 540 return perms; 541 } 542 543 } 544