1 /*
   2  * Copyright (c) 2009, 2011, 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.InputStreamReader;
  44 import java.net.HttpURLConnection;
  45 import java.net.InetSocketAddress;
  46 import java.net.PasswordAuthentication;
  47 import java.net.Proxy;
  48 import java.net.URL;
  49 import java.security.PrivilegedExceptionAction;
  50 import java.util.HashMap;
  51 import java.util.Map;
  52 import javax.security.auth.Subject;
  53 import org.ietf.jgss.GSSContext;
  54 import org.ietf.jgss.GSSCredential;
  55 import org.ietf.jgss.GSSManager;
  56 import sun.security.jgss.GSSUtil;
  57 import sun.security.krb5.Config;
  58 import java.util.Base64;
  59 
  60 /**
  61  * Basic JGSS/krb5 test with 3 parties: client, server, backend server. Each
  62  * party uses JAAS login to get subjects and executes JGSS calls using
  63  * Subject.doAs.
  64  */
  65 public class HttpNegotiateServer {
  66 
  67     // Two realm, web server in one, proxy server in another
  68     final static String REALM_WEB = "WEB.DOMAIN";
  69     final static String REALM_PROXY = "PROXY.DOMAIN";
  70     final static String KRB5_CONF = "web.conf";
  71     final static String KRB5_TAB = "web.ktab";
  72 
  73     // user principals
  74     final static String WEB_USER = "web";
  75     final static char[] WEB_PASS = "webby".toCharArray();
  76     final static String PROXY_USER = "pro";
  77     final static char[] PROXY_PASS = "proxy".toCharArray();
  78 
  79 
  80     final static String WEB_HOST = "host.web.domain";
  81     final static String PROXY_HOST = "host.proxy.domain";
  82 
  83     // web page content
  84     final static String CONTENT = "Hello, World!";
  85 
  86     // For 6829283, count how many times the Authenticator is called.
  87     static int count = 0;
  88 
  89     static int webPort, proxyPort;
  90 
  91     // URLs for web test, proxy test. The proxy server is not a real proxy
  92     // since it fakes the same content for any URL. :)
  93     static URL webUrl, proxyUrl;
  94 
  95     /**
  96      * This Authenticator checks everything:
  97      * scheme, protocol, requestor type, host, port, and url
  98      */
  99     static class KnowAllAuthenticator extends java.net.Authenticator {
 100         public PasswordAuthentication getPasswordAuthentication () {
 101             if (!getRequestingScheme().equalsIgnoreCase("Negotiate")) {
 102                 throw new RuntimeException("Bad scheme");
 103             }
 104             if (!getRequestingProtocol().equalsIgnoreCase("HTTP")) {
 105                 throw new RuntimeException("Bad protocol");
 106             }
 107             if (getRequestorType() == RequestorType.SERVER) {
 108                 if (!this.getRequestingHost().equalsIgnoreCase(webUrl.getHost())) {
 109                     throw new RuntimeException("Bad host");
 110                 }
 111                 if (this.getRequestingPort() != webUrl.getPort()) {
 112                     throw new RuntimeException("Bad port");
 113                 }
 114                 if (!this.getRequestingURL().equals(webUrl)) {
 115                     throw new RuntimeException("Bad url");
 116                 }
 117                 return new PasswordAuthentication(
 118                         WEB_USER+"@"+REALM_WEB, WEB_PASS);
 119             } else if (getRequestorType() == RequestorType.PROXY) {
 120                 if (!this.getRequestingHost().equalsIgnoreCase(PROXY_HOST)) {
 121                     throw new RuntimeException("Bad host");
 122                 }
 123                 if (this.getRequestingPort() != proxyPort) {
 124                     throw new RuntimeException("Bad port");
 125                 }
 126                 if (!this.getRequestingURL().equals(proxyUrl)) {
 127                     throw new RuntimeException("Bad url");
 128                 }
 129                 return new PasswordAuthentication(
 130                         PROXY_USER+"@"+REALM_PROXY, PROXY_PASS);
 131             } else  {
 132                 throw new RuntimeException("Bad requster type");
 133             }
 134         }
 135     }
 136 
 137     /**
 138      * This Authenticator knows nothing
 139      */
 140     static class KnowNothingAuthenticator extends java.net.Authenticator {
 141         @Override
 142         public PasswordAuthentication getPasswordAuthentication () {
 143             HttpNegotiateServer.count++;
 144             return null;
 145         }
 146     }
 147 
 148     public static void main(String[] args)
 149             throws Exception {
 150 
 151         KDC kdcw = KDC.create(REALM_WEB);
 152         kdcw.addPrincipal(WEB_USER, WEB_PASS);
 153         kdcw.addPrincipalRandKey("krbtgt/" + REALM_WEB);
 154         kdcw.addPrincipalRandKey("HTTP/" + WEB_HOST);
 155 
 156         KDC kdcp = KDC.create(REALM_PROXY);
 157         kdcp.addPrincipal(PROXY_USER, PROXY_PASS);
 158         kdcp.addPrincipalRandKey("krbtgt/" + REALM_PROXY);
 159         kdcp.addPrincipalRandKey("HTTP/" + PROXY_HOST);
 160 
 161         KDC.saveConfig(KRB5_CONF, kdcw, kdcp,
 162                 "default_keytab_name = " + KRB5_TAB,
 163                 "[domain_realm]",
 164                 "",
 165                 ".web.domain="+REALM_WEB,
 166                 ".proxy.domain="+REALM_PROXY);
 167 
 168         System.setProperty("java.security.krb5.conf", KRB5_CONF);
 169         Config.refresh();
 170         KDC.writeMultiKtab(KRB5_TAB, kdcw, kdcp);
 171 
 172         // Write a customized JAAS conf file, so that any kinit cache
 173         // will be ignored.
 174         System.setProperty("java.security.auth.login.config", OneKDC.JAAS_CONF);
 175         File f = new File(OneKDC.JAAS_CONF);
 176         FileOutputStream fos = new FileOutputStream(f);
 177         fos.write((
 178                 "com.sun.security.jgss.krb5.initiate {\n" +
 179                 "    com.sun.security.auth.module.Krb5LoginModule required;\n};\n"
 180                 ).getBytes());
 181         fos.close();
 182 
 183         HttpServer h1 = httpd("Negotiate", false,
 184                 "HTTP/" + WEB_HOST + "@" + REALM_WEB, KRB5_TAB);
 185         webPort = h1.getAddress().getPort();
 186         HttpServer h2 = httpd("Negotiate", true,
 187                 "HTTP/" + PROXY_HOST + "@" + REALM_PROXY, KRB5_TAB);
 188         proxyPort = h2.getAddress().getPort();
 189 
 190         webUrl = new URL("http://" + WEB_HOST +":" + webPort + "/a/b/c");
 191         proxyUrl = new URL("http://nosuchplace/a/b/c");
 192 
 193         try {
 194             Exception e1 = null, e2 = null;
 195             try {
 196                 test6578647();
 197             } catch (Exception e) {
 198                 e1 = e;
 199                 e.printStackTrace();
 200             }
 201             try {
 202                 test6829283();
 203             } catch (Exception e) {
 204                 e2 = e;
 205                 e.printStackTrace();
 206             }
 207             if (e1 != null || e2 != null) {
 208                 throw new RuntimeException("Test error");
 209             }
 210         } finally {
 211             // Must stop. Seems there's no HttpServer.startAsDaemon()
 212             if (h1 != null) h1.stop(0);
 213             if (h2 != null) h2.stop(0);
 214         }
 215     }
 216 
 217     static void test6578647() throws Exception {
 218         BufferedReader reader;
 219         java.net.Authenticator.setDefault(new KnowAllAuthenticator());
 220 
 221         reader = new BufferedReader(new InputStreamReader(
 222                 webUrl.openConnection().getInputStream()));
 223         if (!reader.readLine().equals(CONTENT)) {
 224             throw new RuntimeException("Bad content");
 225         }
 226 
 227         reader = new BufferedReader(new InputStreamReader(
 228                 proxyUrl.openConnection(
 229                 new Proxy(Proxy.Type.HTTP,
 230                     new InetSocketAddress(PROXY_HOST, proxyPort)))
 231                 .getInputStream()));
 232         if (!reader.readLine().equals(CONTENT)) {
 233             throw new RuntimeException("Bad content");
 234         }
 235     }
 236 
 237     static void test6829283() throws Exception {
 238         BufferedReader reader;
 239         java.net.Authenticator.setDefault(new KnowNothingAuthenticator());
 240         try {
 241             new BufferedReader(new InputStreamReader(
 242                     webUrl.openConnection().getInputStream()));
 243         } catch (IOException ioe) {
 244             // Will fail since no username and password is provided.
 245         }
 246         if (count > 1) {
 247             throw new RuntimeException("Authenticator called twice");
 248         }
 249     }
 250 
 251     /**
 252      * Creates and starts an HTTP or proxy server that requires
 253      * Negotiate authentication.
 254      * @param scheme "Negotiate" or "Kerberos"
 255      * @param principal the krb5 service principal the server runs with
 256      * @return the server
 257      */
 258     public static HttpServer httpd(String scheme, boolean proxy,
 259             String principal, String ktab) throws Exception {
 260         MyHttpHandler h = new MyHttpHandler();
 261         HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
 262         HttpContext hc = server.createContext("/", h);
 263         hc.setAuthenticator(new MyServerAuthenticator(
 264                 proxy, scheme, principal, ktab));
 265         server.start();
 266         return server;
 267     }
 268 
 269     static class MyHttpHandler implements HttpHandler {
 270         public void handle(HttpExchange t) throws IOException {
 271             t.sendResponseHeaders(200, 0);
 272             t.getResponseBody().write(CONTENT.getBytes());
 273             t.close();
 274         }
 275     }
 276 
 277     static class MyServerAuthenticator
 278             extends com.sun.net.httpserver.Authenticator {
 279         Subject s = new Subject();
 280         GSSManager m = null;
 281         GSSCredential cred = null;
 282         String scheme = null;
 283         String reqHdr = "WWW-Authenticate";
 284         String respHdr = "Authorization";
 285         int err = HttpURLConnection.HTTP_UNAUTHORIZED;
 286 
 287         public MyServerAuthenticator(boolean proxy, String scheme,
 288                 String principal, String ktab) throws Exception {
 289 
 290             this.scheme = scheme;
 291             if (proxy) {
 292                 reqHdr = "Proxy-Authenticate";
 293                 respHdr = "Proxy-Authorization";
 294                 err = HttpURLConnection.HTTP_PROXY_AUTH;
 295             }
 296 
 297             Krb5LoginModule krb5 = new Krb5LoginModule();
 298             Map<String, String> map = new HashMap<>();
 299             Map<String, Object> shared = new HashMap<>();
 300 
 301             map.put("storeKey", "true");
 302             map.put("isInitiator", "false");
 303             map.put("useKeyTab", "true");
 304             map.put("keyTab", ktab);
 305             map.put("principal", principal);
 306             krb5.initialize(s, null, shared, map);
 307             krb5.login();
 308             krb5.commit();
 309             m = GSSManager.getInstance();
 310             cred = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() {
 311                 @Override
 312                 public GSSCredential run() throws Exception {
 313                     System.err.println("Creating GSSCredential");
 314                     return m.createCredential(
 315                             null,
 316                             GSSCredential.INDEFINITE_LIFETIME,
 317                             MyServerAuthenticator.this.scheme.equalsIgnoreCase("Negotiate")?
 318                                     GSSUtil.GSS_SPNEGO_MECH_OID:
 319                                     GSSUtil.GSS_KRB5_MECH_OID,
 320                             GSSCredential.ACCEPT_ONLY);
 321                 }
 322             });
 323         }
 324 
 325         @Override
 326         public Result authenticate(HttpExchange exch) {
 327             // The GSContext is stored in an HttpContext attribute named
 328             // "GSSContext" and is created at the first request.
 329             GSSContext c = null;
 330             String auth = exch.getRequestHeaders().getFirst(respHdr);
 331             try {
 332                 c = (GSSContext)exch.getHttpContext().getAttributes().get("GSSContext");
 333                 if (auth == null) {                 // First request
 334                     Headers map = exch.getResponseHeaders();
 335                     map.set (reqHdr, scheme);        // Challenge!
 336                     c = Subject.doAs(s, new PrivilegedExceptionAction<GSSContext>() {
 337                         @Override
 338                         public GSSContext run() throws Exception {
 339                             return m.createContext(cred);
 340                         }
 341                     });
 342                     exch.getHttpContext().getAttributes().put("GSSContext", c);
 343                     return new com.sun.net.httpserver.Authenticator.Retry(err);
 344                 } else {                            // Later requests
 345                     byte[] token = Base64.getMimeDecoder().decode(auth.split(" ")[1]);
 346                     token = c.acceptSecContext(token, 0, token.length);
 347                     Headers map = exch.getResponseHeaders();
 348                     map.set (reqHdr, scheme + " " + Base64.getMimeEncoder()
 349                             .encodeToString(token).replaceAll("\\s", ""));
 350                     if (c.isEstablished()) {
 351                         return new com.sun.net.httpserver.Authenticator.Success(
 352                                 new HttpPrincipal(c.getSrcName().toString(), ""));
 353                     } else {
 354                         return new com.sun.net.httpserver.Authenticator.Retry(err);
 355                     }
 356                 }
 357             } catch (Exception e) {
 358                 throw new RuntimeException(e);
 359             }
 360         }
 361     }
 362 }