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