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 }