1 /*
2  * Copyright (c) 2009, 2018, 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 8194486
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.jgss.krb5
32  *          java.security.jgss/sun.security.krb5.internal.ccache
33  *          java.security.jgss/sun.security.krb5.internal.crypto
34  *          java.security.jgss/sun.security.krb5.internal.ktab
35  *          jdk.security.auth
36  *          jdk.security.jgss
37  *          jdk.httpserver
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  * @library /test/lib
43  * @run main jdk.test.lib.FileInstaller TestHosts TestHosts
44  * @run main/othervm -Djdk.net.hosts.file=TestHosts HttpNegotiateServer
45  */
46 
47 import com.sun.net.httpserver.Headers;
48 import com.sun.net.httpserver.HttpContext;
49 import com.sun.net.httpserver.HttpExchange;
50 import com.sun.net.httpserver.HttpHandler;
51 import com.sun.net.httpserver.HttpServer;
52 import com.sun.net.httpserver.HttpPrincipal;
53 import com.sun.security.auth.module.Krb5LoginModule;
54 import java.io.BufferedReader;
55 import java.io.File;
56 import java.io.FileOutputStream;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.io.InputStreamReader;
60 import java.net.HttpURLConnection;
61 import java.net.InetSocketAddress;
62 import java.net.PasswordAuthentication;
63 import java.net.Proxy;
64 import java.net.URL;
65 import java.net.URLConnection;
66 import java.security.*;
67 import java.util.HashMap;
68 import java.util.Map;
69 import javax.security.auth.Subject;
70 import javax.security.auth.callback.Callback;
71 import javax.security.auth.callback.CallbackHandler;
72 import javax.security.auth.callback.NameCallback;
73 import javax.security.auth.callback.PasswordCallback;
74 import javax.security.auth.callback.UnsupportedCallbackException;
75 import javax.security.auth.login.AppConfigurationEntry;
76 import javax.security.auth.login.Configuration;
77 import javax.security.auth.login.LoginContext;
78 import javax.security.auth.login.LoginException;
79 import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
80 import org.ietf.jgss.GSSContext;
81 import org.ietf.jgss.GSSCredential;
82 import org.ietf.jgss.GSSManager;
83 import sun.security.jgss.GSSUtil;
84 import sun.security.krb5.Config;
85 import java.util.Base64;
86 
87 /**
88  * Basic JGSS/krb5 test with 3 parties: client, server, backend server. Each
89  * party uses JAAS login to get subjects and executes JGSS calls using
90  * Subject.doAs.
91  */
92 public class HttpNegotiateServer {
93 
94     // Two realm, web server in one, proxy server in another
95     final static String REALM_WEB = "WEB.DOMAIN";
96     final static String REALM_PROXY = "PROXY.DOMAIN";
97     final static String KRB5_CONF = "web.conf";
98     final static String KRB5_TAB = "web.ktab";
99 
100     // user principals
101     final static String WEB_USER = "web";
102     final static char[] WEB_PASS = "webby".toCharArray();
103     final static String PROXY_USER = "pro";
104     final static char[] PROXY_PASS = "proxy".toCharArray();
105 
106 
107     final static String WEB_HOST = "host.web.domain";
108     final static String PROXY_HOST = "host.proxy.domain";
109 
110     // web page content
111     final static String CONTENT = "Hello, World!";
112 
113     // For 6829283, count how many times the Authenticator is called.
114     static int count = 0;
115 
116     static int webPort, proxyPort;
117 
118     // URLs for web test, proxy test. The proxy server is not a real proxy
119     // since it fakes the same content for any URL. :)
120     static URL webUrl, proxyUrl;
121 
122     /**
123      * This Authenticator checks everything:
124      * scheme, protocol, requestor type, host, port, and url
125      */
126     static class KnowAllAuthenticator extends java.net.Authenticator {
getPasswordAuthentication()127         public PasswordAuthentication getPasswordAuthentication () {
128             if (!getRequestingScheme().equalsIgnoreCase("Negotiate")) {
129                 throw new RuntimeException("Bad scheme");
130             }
131             if (!getRequestingProtocol().equalsIgnoreCase("HTTP")) {
132                 throw new RuntimeException("Bad protocol");
133             }
134             if (getRequestorType() == RequestorType.SERVER) {
135                 if (!this.getRequestingHost().equalsIgnoreCase(webUrl.getHost())) {
136                     throw new RuntimeException("Bad host");
137                 }
138                 if (this.getRequestingPort() != webUrl.getPort()) {
139                     throw new RuntimeException("Bad port");
140                 }
141                 if (!this.getRequestingURL().equals(webUrl)) {
142                     throw new RuntimeException("Bad url");
143                 }
144                 return new PasswordAuthentication(
145                         WEB_USER+"@"+REALM_WEB, WEB_PASS);
146             } else if (getRequestorType() == RequestorType.PROXY) {
147                 if (!this.getRequestingHost().equalsIgnoreCase(PROXY_HOST)) {
148                     throw new RuntimeException("Bad host");
149                 }
150                 if (this.getRequestingPort() != proxyPort) {
151                     throw new RuntimeException("Bad port");
152                 }
153                 if (!this.getRequestingURL().equals(proxyUrl)) {
154                     throw new RuntimeException("Bad url");
155                 }
156                 return new PasswordAuthentication(
157                         PROXY_USER+"@"+REALM_PROXY, PROXY_PASS);
158             } else  {
159                 throw new RuntimeException("Bad requster type");
160             }
161         }
162     }
163 
164     /**
165      * This Authenticator knows nothing
166      */
167     static class KnowNothingAuthenticator extends java.net.Authenticator {
168         @Override
getPasswordAuthentication()169         public PasswordAuthentication getPasswordAuthentication () {
170             HttpNegotiateServer.count++;
171             return null;
172         }
173     }
174 
main(String[] args)175     public static void main(String[] args)
176             throws Exception {
177 
178         System.setProperty("sun.security.krb5.debug", "true");
179 
180         KDC kdcw = KDC.create(REALM_WEB);
181         kdcw.addPrincipal(WEB_USER, WEB_PASS);
182         kdcw.addPrincipalRandKey("krbtgt/" + REALM_WEB);
183         kdcw.addPrincipalRandKey("HTTP/" + WEB_HOST);
184 
185         KDC kdcp = KDC.create(REALM_PROXY);
186         kdcp.addPrincipal(PROXY_USER, PROXY_PASS);
187         kdcp.addPrincipalRandKey("krbtgt/" + REALM_PROXY);
188         kdcp.addPrincipalRandKey("HTTP/" + PROXY_HOST);
189 
190         KDC.saveConfig(KRB5_CONF, kdcw, kdcp,
191                 "default_keytab_name = " + KRB5_TAB,
192                 "[domain_realm]",
193                 "",
194                 ".web.domain="+REALM_WEB,
195                 ".proxy.domain="+REALM_PROXY);
196 
197         System.setProperty("java.security.krb5.conf", KRB5_CONF);
198         Config.refresh();
199         KDC.writeMultiKtab(KRB5_TAB, kdcw, kdcp);
200 
201         // Write a customized JAAS conf file, so that any kinit cache
202         // will be ignored.
203         System.setProperty("java.security.auth.login.config", OneKDC.JAAS_CONF);
204         File f = new File(OneKDC.JAAS_CONF);
205         FileOutputStream fos = new FileOutputStream(f);
206         fos.write((
207                 "com.sun.security.jgss.krb5.initiate {\n" +
208                 "    com.sun.security.auth.module.Krb5LoginModule required;\n};\n"
209                 ).getBytes());
210         fos.close();
211 
212         HttpServer h1 = httpd("Negotiate", false,
213                 "HTTP/" + WEB_HOST + "@" + REALM_WEB, KRB5_TAB);
214         webPort = h1.getAddress().getPort();
215         HttpServer h2 = httpd("Negotiate", true,
216                 "HTTP/" + PROXY_HOST + "@" + REALM_PROXY, KRB5_TAB);
217         proxyPort = h2.getAddress().getPort();
218 
219         webUrl = new URL("http://" + WEB_HOST +":" + webPort + "/a/b/c");
220         proxyUrl = new URL("http://nosuchplace/a/b/c");
221 
222         try {
223             Exception e1 = null, e2 = null, e3 = null;
224             try {
225                 test6578647();
226             } catch (Exception e) {
227                 e1 = e;
228                 e.printStackTrace();
229             }
230             try {
231                 test6829283();
232             } catch (Exception e) {
233                 e2 = e;
234                 e.printStackTrace();
235             }
236             try {
237                 test8077155();
238             } catch (Exception e) {
239                 e3 = e;
240                 e.printStackTrace();
241             }
242 
243             if (e1 != null || e2 != null || e3 != null) {
244                 throw new RuntimeException("Test error");
245             }
246         } finally {
247             // Must stop. Seems there's no HttpServer.startAsDaemon()
248             if (h1 != null) h1.stop(0);
249             if (h2 != null) h2.stop(0);
250         }
251     }
252 
test6578647()253     static void test6578647() throws Exception {
254         BufferedReader reader;
255         java.net.Authenticator.setDefault(new KnowAllAuthenticator());
256 
257         reader = new BufferedReader(new InputStreamReader(
258                 webUrl.openConnection(Proxy.NO_PROXY).getInputStream()));
259         if (!reader.readLine().equals(CONTENT)) {
260             throw new RuntimeException("Bad content");
261         }
262 
263         reader = new BufferedReader(new InputStreamReader(
264                 proxyUrl.openConnection(new Proxy(Proxy.Type.HTTP,
265                                 new InetSocketAddress(PROXY_HOST, proxyPort)))
266                         .getInputStream()));
267         if (!reader.readLine().equals(CONTENT)) {
268             throw new RuntimeException("Bad content");
269         }
270     }
271 
test6829283()272     static void test6829283() throws Exception {
273         BufferedReader reader;
274         java.net.Authenticator.setDefault(new KnowNothingAuthenticator());
275         try {
276             new BufferedReader(new InputStreamReader(
277                     webUrl.openConnection(Proxy.NO_PROXY).getInputStream()));
278         } catch (IOException ioe) {
279             // Will fail since no username and password is provided.
280         }
281         if (count > 1) {
282             throw new RuntimeException("Authenticator called twice");
283         }
284     }
285 
testConnect()286     static void testConnect() {
287         InputStream inputStream = null;
288         try {
289             URL url = webUrl;
290 
291             URLConnection conn = url.openConnection(Proxy.NO_PROXY);
292             conn.connect();
293             inputStream = conn.getInputStream();
294             byte[] b = new byte[inputStream.available()];
295             for (int j = 0; j < b.length; j++) {
296                 b[j] = (byte) inputStream.read();
297             }
298             String s = new String(b);
299             System.out.println("Length: " + s.length());
300             System.out.println(s);
301         } catch (Exception ex) {
302             throw new RuntimeException(ex);
303         } finally {
304             if (inputStream != null) {
305                 try {
306                     inputStream.close();
307                 } catch (IOException e) {
308                     e.printStackTrace();
309                 }
310             }
311         }
312     }
313 
test8077155()314     static void test8077155() throws Exception {
315         final String username = WEB_USER;
316         final char[] password = WEB_PASS;
317 
318         SecurityManager security = new SecurityManager();
319         Policy.setPolicy(new SecurityPolicy());
320         System.setSecurityManager(security);
321 
322         CallbackHandler callback = new CallbackHandler() {
323             @Override
324             public void handle(Callback[] pCallbacks)
325                     throws IOException, UnsupportedCallbackException {
326                 for (Callback cb : pCallbacks) {
327                     if (cb instanceof NameCallback) {
328                         NameCallback ncb = (NameCallback)cb;
329                         ncb.setName(username);
330 
331                     } else  if (cb instanceof PasswordCallback) {
332                         PasswordCallback pwdcb = (PasswordCallback) cb;
333                         pwdcb.setPassword(password);
334                     }
335                 }
336             }
337 
338         };
339 
340         final String jaasConfigName = "oracle.test.kerberos.login";
341         final String krb5LoginModule
342                 = "com.sun.security.auth.module.Krb5LoginModule";
343 
344         Configuration loginConfig = new Configuration() {
345             @Override
346             public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
347                 if (! jaasConfigName.equals(name)) {
348                     return new AppConfigurationEntry[0];
349                 }
350 
351                 Map<String, String> options = new HashMap<String, String>();
352                 options.put("useTicketCache", Boolean.FALSE.toString());
353                 options.put("useKeyTab", Boolean.FALSE.toString());
354 
355                 return new AppConfigurationEntry[] {
356                         new AppConfigurationEntry(krb5LoginModule,
357                                 LoginModuleControlFlag.REQUIRED,
358                                 options)
359                         };
360             }
361         };
362 
363         // oracle context/subject/login
364         LoginContext context = null;
365         try {
366             context = new LoginContext(
367                     "oracle.test.kerberos.login", null, callback, loginConfig);
368             context.login();
369 
370         } catch (LoginException ex) {
371             ex.printStackTrace();
372             throw new RuntimeException(ex);
373         }
374 
375 
376         Subject subject = context.getSubject();
377 
378         final PrivilegedExceptionAction<Object> test_action
379                 = new PrivilegedExceptionAction<Object>() {
380             public Object run() throws Exception {
381                 testConnect();
382                 return null;
383             }
384         };
385 
386         System.err.println("\n\nExpecting to succeed when executing " +
387                 "with the the logged in subject.");
388 
389         try {
390             Subject.doAs(subject, test_action);
391             System.err.println("\n\nConnection succeed when executing " +
392                     "with the the logged in subject.");
393         } catch (PrivilegedActionException e) {
394             System.err.println("\n\nFailure unexpected when executing " +
395                     "with the the logged in subject.");
396             e.printStackTrace();
397             throw new RuntimeException("Failed to login as subject");
398         }
399 
400         try {
401             System.err.println("\n\nExpecting to fail when running " +
402                     "with the current user's login.");
403             testConnect();
404         } catch (Exception ex) {
405             System.err.println("\nConnect failed when running " +
406                     "with the current user's login:\n" + ex.getMessage());
407         }
408     }
409 
410     /**
411      * Creates and starts an HTTP or proxy server that requires
412      * Negotiate authentication.
413      * @param scheme "Negotiate" or "Kerberos"
414      * @param principal the krb5 service principal the server runs with
415      * @return the server
416      */
httpd(String scheme, boolean proxy, String principal, String ktab)417     public static HttpServer httpd(String scheme, boolean proxy,
418             String principal, String ktab) throws Exception {
419         MyHttpHandler h = new MyHttpHandler();
420         HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
421         HttpContext hc = server.createContext("/", h);
422         hc.setAuthenticator(new MyServerAuthenticator(
423                 proxy, scheme, principal, ktab));
424         server.start();
425         return server;
426     }
427 
428     static class MyHttpHandler implements HttpHandler {
handle(HttpExchange t)429         public void handle(HttpExchange t) throws IOException {
430             t.sendResponseHeaders(200, 0);
431             t.getResponseBody().write(CONTENT.getBytes());
432             t.close();
433         }
434     }
435 
436     static class MyServerAuthenticator
437             extends com.sun.net.httpserver.Authenticator {
438         Subject s = new Subject();
439         GSSManager m = null;
440         GSSCredential cred = null;
441         String scheme = null;
442         String reqHdr = "WWW-Authenticate";
443         String respHdr = "Authorization";
444         int err = HttpURLConnection.HTTP_UNAUTHORIZED;
445 
MyServerAuthenticator(boolean proxy, String scheme, String principal, String ktab)446         public MyServerAuthenticator(boolean proxy, String scheme,
447                 String principal, String ktab) throws Exception {
448 
449             this.scheme = scheme;
450             if (proxy) {
451                 reqHdr = "Proxy-Authenticate";
452                 respHdr = "Proxy-Authorization";
453                 err = HttpURLConnection.HTTP_PROXY_AUTH;
454             }
455 
456             Krb5LoginModule krb5 = new Krb5LoginModule();
457             Map<String, String> map = new HashMap<>();
458             Map<String, Object> shared = new HashMap<>();
459 
460             map.put("storeKey", "true");
461             map.put("isInitiator", "false");
462             map.put("useKeyTab", "true");
463             map.put("keyTab", ktab);
464             map.put("principal", principal);
465             krb5.initialize(s, null, shared, map);
466             krb5.login();
467             krb5.commit();
468             m = GSSManager.getInstance();
469             cred = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() {
470                 @Override
471                 public GSSCredential run() throws Exception {
472                     System.err.println("Creating GSSCredential");
473                     return m.createCredential(
474                             null,
475                             GSSCredential.INDEFINITE_LIFETIME,
476                             MyServerAuthenticator.this.scheme
477                                         .equalsIgnoreCase("Negotiate") ?
478                                     GSSUtil.GSS_SPNEGO_MECH_OID :
479                                     GSSUtil.GSS_KRB5_MECH_OID,
480                             GSSCredential.ACCEPT_ONLY);
481                 }
482             });
483         }
484 
485         @Override
authenticate(HttpExchange exch)486         public Result authenticate(HttpExchange exch) {
487             // The GSContext is stored in an HttpContext attribute named
488             // "GSSContext" and is created at the first request.
489             GSSContext c = null;
490             String auth = exch.getRequestHeaders().getFirst(respHdr);
491             try {
492                 c = (GSSContext)exch.getHttpContext()
493                         .getAttributes().get("GSSContext");
494                 if (auth == null) {                 // First request
495                     Headers map = exch.getResponseHeaders();
496                     map.set (reqHdr, scheme);        // Challenge!
497                     c = Subject.doAs(s, new PrivilegedExceptionAction<GSSContext>() {
498                         @Override
499                         public GSSContext run() throws Exception {
500                             return m.createContext(cred);
501                         }
502                     });
503                     exch.getHttpContext().getAttributes().put("GSSContext", c);
504                     return new com.sun.net.httpserver.Authenticator.Retry(err);
505                 } else {                            // Later requests
506                     byte[] token = Base64.getMimeDecoder()
507                             .decode(auth.split(" ")[1]);
508                     token = c.acceptSecContext(token, 0, token.length);
509                     Headers map = exch.getResponseHeaders();
510                     map.set (reqHdr, scheme + " " + Base64.getMimeEncoder()
511                             .encodeToString(token).replaceAll("\\s", ""));
512                     if (c.isEstablished()) {
513                         return new com.sun.net.httpserver.Authenticator.Success(
514                                 new HttpPrincipal(c.getSrcName().toString(), ""));
515                     } else {
516                         return new com.sun.net.httpserver.Authenticator.Retry(err);
517                     }
518                 }
519             } catch (Exception e) {
520                 throw new RuntimeException(e);
521             }
522         }
523     }
524 }
525 
526 class SecurityPolicy extends Policy {
527 
528     private static Permissions perms;
529 
SecurityPolicy()530     public SecurityPolicy() {
531         super();
532         if (perms == null) {
533             perms = new Permissions();
534             perms.add(new AllPermission());
535         }
536     }
537 
538     @Override
getPermissions(CodeSource codesource)539     public PermissionCollection getPermissions(CodeSource codesource) {
540         return perms;
541     }
542 
543 }
544