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