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