DigestAuth.java revision 13536:d354886acd3f
1/*
2 * Copyright (c) 2016, 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
24import com.sun.net.httpserver.HttpExchange;
25import com.sun.net.httpserver.HttpHandler;
26import com.sun.net.httpserver.HttpServer;
27import java.io.BufferedReader;
28import java.io.InputStreamReader;
29import java.io.IOException;
30import java.io.InputStream;
31import java.net.Authenticator;
32import java.net.InetSocketAddress;
33import java.net.PasswordAuthentication;
34import java.net.URL;
35import java.net.URLConnection;
36import java.util.List;
37
38/*
39 * @test
40 * @bug 8138990
41 * @summary Tests for HTTP Digest auth
42 *          The impl maintains a cache for auth info,
43 *          the testcases run in a separate JVM to avoid cache hits
44 * @run main/othervm DigestAuth good
45 * @run main/othervm DigestAuth only_nonce
46 * @run main/othervm DigestAuth sha1
47 * @run main/othervm DigestAuth no_header
48 * @run main/othervm DigestAuth no_nonce
49 * @run main/othervm DigestAuth no_qop
50 * @run main/othervm DigestAuth invalid_alg
51 * @run main/othervm DigestAuth validate_server
52 * @run main/othervm DigestAuth validate_server_no_qop
53 */
54public class DigestAuth {
55
56    static final String LOCALHOST = "localhost";
57    static final String EXPECT_FAILURE = null;
58    static final String EXPECT_DIGEST = "Digest";
59    static final String REALM = "testrealm@host.com";
60    static final String NEXT_NONCE = "40f2e879449675f288476d772627370a";
61
62    static final String GOOD_WWW_AUTH_HEADER = "Digest "
63            + "realm=\"testrealm@host.com\", "
64            + "qop=\"auth,auth-int\", "
65            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
66            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
67
68    static final String GOOD_WWW_AUTH_HEADER_NO_QOP = "Digest "
69            + "realm=\"testrealm@host.com\", "
70            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
71            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
72
73    static final String WWW_AUTH_HEADER_NO_NONCE = "Digest "
74            + "realm=\"testrealm@host.com\", "
75            + "qop=\"auth,auth-int\", "
76            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
77
78    static final String WWW_AUTH_HEADER_NO_QOP = "Digest "
79            + "realm=\"testrealm@host.com\", "
80            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
81            + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
82
83    static final String WWW_AUTH_HEADER_ONLY_NONCE = "Digest "
84            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"";
85
86    static final String WWW_AUTH_HEADER_SHA1 = "Digest "
87            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
88            + "algorithm=\"SHA1\"";
89
90    static final String WWW_AUTH_HEADER_INVALID_ALGORITHM = "Digest "
91            + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
92            + "algorithm=\"SHA123\"";
93
94    static final String AUTH_INFO_HEADER_NO_QOP_FIRST =
95              "nextnonce=\"" + NEXT_NONCE + "\", "
96            + "rspauth=\"ee85bc4315d8b18757809f1a8b9382d8\"";
97
98    static final String AUTH_INFO_HEADER_NO_QOP_SECOND =
99              "rspauth=\"12f2fa12841b3775b6054576722446b2\"";
100
101    static final String AUTH_INFO_HEADER_WRONG_DIGEST =
102              "nextnonce=\"" + NEXT_NONCE + "\", "
103            + "rspauth=\"7327570c586207eca2afae94fc20903d\", "
104            + "cnonce=\"0a4f113b\", "
105            + "nc=00000001, "
106            + "qop=auth";
107
108    public static void main(String[] args) throws Exception {
109        if (args.length == 0) {
110            throw new RuntimeException("No testcase specified");
111        }
112        String testcase = args[0];
113
114        // start a local HTTP server
115        try (LocalHttpServer server = LocalHttpServer.startServer()) {
116
117            // set authenticator
118            AuthenticatorImpl auth = new AuthenticatorImpl();
119            Authenticator.setDefault(auth);
120
121            String url = String.format("http://%s:%d/test/",
122                    LOCALHOST, server.getPort());
123
124            boolean success = true;
125            switch (testcase) {
126                case "good":
127                    // server returns a good WWW-Authenticate header
128                    server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
129                    success = testAuth(url, auth, EXPECT_DIGEST);
130                    if (auth.lastRequestedPrompt == null ||
131                            !auth.lastRequestedPrompt.equals(REALM)) {
132                        System.out.println("Unexpected realm: "
133                                + auth.lastRequestedPrompt);
134                        success = false;
135                    }
136                    break;
137                case "validate_server":
138                    // enable processing Authentication-Info headers
139                    System.setProperty("http.auth.digest.validateServer",
140                            "true");
141
142                    /* Server returns good WWW-Authenticate
143                     * and Authentication-Info headers with wrong digest
144                     */
145                    server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
146                    server.setAuthInfoHeader(AUTH_INFO_HEADER_WRONG_DIGEST);
147                    success = testAuth(url, auth, EXPECT_FAILURE);
148                    if (auth.lastRequestedPrompt == null ||
149                            !auth.lastRequestedPrompt.equals(REALM)) {
150                        System.out.println("Unexpected realm: "
151                                + auth.lastRequestedPrompt);
152                        success = false;
153                    }
154                    break;
155                case "validate_server_no_qop":
156                    // enable processing Authentication-Info headers
157                    System.setProperty("http.auth.digest.validateServer",
158                            "true");
159
160                    /* Server returns good both WWW-Authenticate
161                     * and Authentication-Info headers without any qop field,
162                     * so that client-nonce should not be taked into account,
163                     * and connection should succeed.
164                     */
165                    server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER_NO_QOP);
166                    server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_FIRST);
167                    success = testAuth(url, auth, EXPECT_DIGEST);
168                    if (auth.lastRequestedPrompt == null ||
169                            !auth.lastRequestedPrompt.equals(REALM)) {
170                        System.out.println("Unexpected realm: "
171                                + auth.lastRequestedPrompt);
172                        success = false;
173                    }
174
175                    // connect again and check if nextnonce was used
176                    server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_SECOND);
177                    success &= testAuth(url, auth, EXPECT_DIGEST);
178                    if (!NEXT_NONCE.equals(server.lastRequestedNonce)) {
179                        System.out.println("Unexpected next nonce: "
180                                + server.lastRequestedNonce);
181                        success = false;
182                    }
183                    break;
184                case "only_nonce":
185                    /* Server returns a good WWW-Authenticate header
186                     * which contains only nonce (no realm set).
187                     *
188                     * Realm from  WWW-Authenticate header is passed to
189                     * authenticator which can use it as a prompt
190                     * when it asks a user for credentials.
191                     *
192                     * It's fine if an HTTP client doesn't fail if no realm set,
193                     * and delegates making a decision to authenticator/user.
194                     */
195                    server.setWWWAuthHeader(WWW_AUTH_HEADER_ONLY_NONCE);
196                    success = testAuth(url, auth, EXPECT_DIGEST);
197                    if (auth.lastRequestedPrompt != null &&
198                            !auth.lastRequestedPrompt.trim().isEmpty()) {
199                        System.out.println("Unexpected realm: "
200                                + auth.lastRequestedPrompt);
201                        success = false;
202                    }
203                    break;
204                case "sha1":
205                    // server returns a good WWW-Authenticate header with SHA-1
206                    server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1);
207                    success = testAuth(url, auth, EXPECT_DIGEST);
208                    break;
209                case "no_header":
210                    // server returns no WWW-Authenticate header
211                    success = testAuth(url, auth, EXPECT_FAILURE);
212                    if (auth.lastRequestedScheme != null) {
213                        System.out.println("Unexpected scheme: "
214                                + auth.lastRequestedScheme);
215                        success = false;
216                    }
217                    break;
218                case "no_nonce":
219                    // server returns a wrong WWW-Authenticate header (no nonce)
220                    server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_NONCE);
221                    success = testAuth(url, auth, EXPECT_FAILURE);
222                    break;
223                case "invalid_alg":
224                    // server returns a wrong WWW-Authenticate header
225                    // (invalid hash algorithm)
226                    server.setWWWAuthHeader(WWW_AUTH_HEADER_INVALID_ALGORITHM);
227                    success = testAuth(url, auth, EXPECT_FAILURE);
228                    break;
229                case "no_qop":
230                    // server returns a good WWW-Authenticate header
231                    // without QOPs
232                    server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_QOP);
233                    success = testAuth(url, auth, EXPECT_DIGEST);
234                    break;
235                default:
236                    throw new RuntimeException("Unexpected testcase: "
237                            + testcase);
238            }
239
240            if (!success) {
241                throw new RuntimeException("Test failed");
242            }
243        }
244
245        System.out.println("Test passed");
246    }
247
248    static boolean testAuth(String url, AuthenticatorImpl auth,
249            String expectedScheme) {
250
251        try {
252            System.out.printf("Connect to %s, expected auth scheme is '%s'%n",
253                    url, expectedScheme);
254            load(url);
255
256            if (expectedScheme == null) {
257                System.out.println("Unexpected successful connection");
258                return false;
259            }
260
261            System.out.printf("Actual auth scheme is '%s'%n",
262                    auth.lastRequestedScheme);
263            if (!expectedScheme.equalsIgnoreCase(auth.lastRequestedScheme)) {
264                System.out.println("Unexpected auth scheme");
265                return false;
266            }
267        } catch (IOException e) {
268            if (expectedScheme != null) {
269                System.out.println("Unexpected exception: " + e);
270                e.printStackTrace(System.out);
271                return false;
272            }
273            System.out.println("Expected exception: " + e);
274        }
275
276        return true;
277    }
278
279    static void load(String url) throws IOException {
280        URLConnection conn = new URL(url).openConnection();
281        conn.setUseCaches(false);
282        try (BufferedReader reader = new BufferedReader(
283                new InputStreamReader(conn.getInputStream()))) {
284
285            String line = reader.readLine();
286            if (line == null) {
287                throw new IOException("Couldn't read response");
288            }
289            do {
290                System.out.println(line);
291            } while ((line = reader.readLine()) != null);
292        }
293    }
294
295    private static class AuthenticatorImpl extends Authenticator {
296
297        private String lastRequestedScheme;
298        private String lastRequestedPrompt;
299
300        @Override
301        public PasswordAuthentication getPasswordAuthentication() {
302            lastRequestedScheme = getRequestingScheme();
303            lastRequestedPrompt = getRequestingPrompt();
304            System.out.println("AuthenticatorImpl: requested "
305                    + lastRequestedScheme);
306
307            return new PasswordAuthentication("Mufasa",
308                    "Circle Of Life".toCharArray());
309        }
310    }
311
312    // local HTTP server which pretends to support HTTP Digest auth
313    static class LocalHttpServer implements HttpHandler, AutoCloseable {
314
315        private final HttpServer server;
316        private volatile String wwwAuthHeader = null;
317        private volatile String authInfoHeader = null;
318        private volatile String lastRequestedNonce;
319
320        private LocalHttpServer(HttpServer server) {
321            this.server = server;
322        }
323
324        void setWWWAuthHeader(String wwwAuthHeader) {
325            this.wwwAuthHeader = wwwAuthHeader;
326        }
327
328        void setAuthInfoHeader(String authInfoHeader) {
329            this.authInfoHeader = authInfoHeader;
330        }
331
332        static LocalHttpServer startServer() throws IOException {
333            HttpServer httpServer = HttpServer.create(
334                    new InetSocketAddress(0), 0);
335            LocalHttpServer localHttpServer = new LocalHttpServer(httpServer);
336            localHttpServer.start();
337
338            return localHttpServer;
339        }
340
341        void start() {
342            server.createContext("/test", this);
343            server.start();
344            System.out.println("HttpServer: started on port " + getPort());
345        }
346
347        void stop() {
348            server.stop(0);
349            System.out.println("HttpServer: stopped");
350        }
351
352        int getPort() {
353            return server.getAddress().getPort();
354        }
355
356        @Override
357        public void handle(HttpExchange t) throws IOException {
358            System.out.println("HttpServer: handle connection");
359
360            // read a request
361            try (InputStream is = t.getRequestBody()) {
362                while (is.read() > 0);
363            }
364
365            try {
366                List<String> headers = t.getRequestHeaders()
367                        .get("Authorization");
368                String header = "";
369                if (headers != null && !headers.isEmpty()) {
370                    header = headers.get(0).trim().toLowerCase();
371                }
372                if (header.startsWith("digest")) {
373                    if (authInfoHeader != null) {
374                        t.getResponseHeaders().add("Authentication-Info",
375                                authInfoHeader);
376                    }
377                    lastRequestedNonce = findParameter(header, "nonce");
378                    byte[] output = "hello".getBytes();
379                    t.sendResponseHeaders(200, output.length);
380                    t.getResponseBody().write(output);
381                    System.out.println("HttpServer: return 200");
382                } else {
383                    if (wwwAuthHeader != null) {
384                        t.getResponseHeaders().add(
385                                "WWW-Authenticate", wwwAuthHeader);
386                    }
387                    byte[] output = "forbidden".getBytes();
388                    t.sendResponseHeaders(401, output.length);
389                    t.getResponseBody().write(output);
390                    System.out.println("HttpServer: return 401");
391                }
392            } catch (IOException e) {
393                System.out.println("HttpServer: exception: " + e);
394                System.out.println("HttpServer: return 500");
395                t.sendResponseHeaders(500, 0);
396            } finally {
397                t.close();
398            }
399        }
400
401        private static String findParameter(String header, String name) {
402            name = name.toLowerCase();
403            if (header != null) {
404                String[] params = header.split("\\s");
405                for (String param : params) {
406                    param = param.trim().toLowerCase();
407                    if (param.startsWith(name)) {
408                        String[] parts = param.split("=");
409                        if (parts.length > 1) {
410                            return parts[1]
411                                    .replaceAll("\"", "").replaceAll(",", "");
412                        }
413                    }
414                }
415            }
416            return null;
417        }
418
419        @Override
420        public void close() {
421            stop();
422        }
423    }
424}
425