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 }