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