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 }