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