| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.authenticator; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import org.apache.catalina.Context; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.connector.Request; |
| import org.apache.catalina.startup.TesterMapRealm; |
| import org.apache.catalina.startup.TesterServlet; |
| import org.apache.catalina.startup.Tomcat; |
| import org.apache.catalina.startup.TomcatBaseTest; |
| import org.apache.tomcat.unittest.TesterContext; |
| import org.apache.tomcat.util.buf.ByteChunk; |
| import org.apache.tomcat.util.descriptor.web.LoginConfig; |
| import org.apache.tomcat.util.descriptor.web.SecurityCollection; |
| import org.apache.tomcat.util.descriptor.web.SecurityConstraint; |
| import org.apache.tomcat.util.security.ConcurrentMessageDigest; |
| import org.apache.tomcat.util.security.MD5Encoder; |
| |
| public class TestDigestAuthenticator extends TomcatBaseTest { |
| |
| private static String USER = "user"; |
| private static String PWD = "pwd"; |
| private static String ROLE = "role"; |
| private static String URI = "/protected"; |
| private static String QUERY = "?foo=bar"; |
| private static String CONTEXT_PATH = "/foo"; |
| private static String CLIENT_AUTH_HEADER = "authorization"; |
| private static String REALM = "TestRealm"; |
| private static String CNONCE = "cnonce"; |
| private static String NC1 = "00000001"; |
| private static String NC2 = "00000002"; |
| private static String QOP = "auth"; |
| |
| |
| @Test |
| public void bug54521() throws LifecycleException { |
| DigestAuthenticator digestAuthenticator = new DigestAuthenticator(); |
| digestAuthenticator.setContainer(new TesterContext()); |
| digestAuthenticator.start(); |
| Request request = new TesterRequest(); |
| final int count = 1000; |
| |
| Set<String> nonces = new HashSet<>(); |
| |
| for (int i = 0; i < count; i++) { |
| nonces.add(digestAuthenticator.generateNonce(request)); |
| } |
| |
| Assert.assertEquals(count, nonces.size()); |
| } |
| |
| |
| @Test |
| public void testAllValid() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| NC1, NC2, CNONCE, QOP, true, true); |
| } |
| |
| @Test |
| public void testValidNoQop() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| null, null, null, null, true, true); |
| } |
| |
| @Test |
| public void testValidQuery() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI + QUERY, false, true, REALM, true, |
| true, NC1, NC2, CNONCE, QOP, true, true); |
| } |
| |
| @Test |
| public void testInvalidUriFail() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, true, true, REALM, true, true, |
| NC1, NC2, CNONCE, QOP, false, false); |
| } |
| |
| @Test |
| public void testInvalidUriPass() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, true, false, REALM, true, true, |
| NC1, NC2, CNONCE, QOP, true, true); |
| } |
| |
| @Test |
| public void testInvalidRealm() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, "null", true, true, |
| NC1, NC2, CNONCE, QOP, false, false); |
| } |
| |
| @Test |
| public void testInvalidNonce() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, false, true, |
| NC1, NC2, CNONCE, QOP, false, true); |
| } |
| |
| @Test |
| public void testInvalidOpaque() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, false, |
| NC1, NC2, CNONCE, QOP, false, true); |
| } |
| |
| @Test |
| public void testInvalidNc1() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| "null", null, CNONCE, QOP, false, false); |
| } |
| |
| @Test |
| public void testInvalidQop() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| NC1, NC2, CNONCE, "null", false, false); |
| } |
| |
| @Test |
| public void testInvalidQopCombo1() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| NC1, NC2, CNONCE, null, false, false); |
| } |
| |
| @Test |
| public void testInvalidQopCombo2() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| NC1, NC2, null, QOP, false, false); |
| } |
| |
| @Test |
| public void testInvalidQopCombo3() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| NC1, NC2, null, null, false, false); |
| } |
| |
| @Test |
| public void testInvalidQopCombo4() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| null, null, CNONCE, QOP, false, false); |
| } |
| |
| @Test |
| public void testInvalidQopCombo5() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| null, null, CNONCE, null, false, false); |
| } |
| |
| @Test |
| public void testInvalidQopCombo6() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| null, null, null, QOP, false, false); |
| } |
| |
| @Test |
| public void testReplay() throws Exception { |
| doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, |
| NC1, NC1, CNONCE, QOP, true, false); |
| } |
| |
| public void doTest(String user, String pwd, String uri, boolean breakUri, |
| boolean validateUri, String realm, boolean useServerNonce, |
| boolean useServerOpaque, String nc1, String nc2, String cnonce, |
| String qop, boolean req2expect200, boolean req3expect200) |
| throws Exception { |
| |
| if (!validateUri) { |
| DigestAuthenticator auth = |
| (DigestAuthenticator) getTomcatInstance().getHost().findChild( |
| CONTEXT_PATH).getPipeline().getFirst(); |
| auth.setValidateUri(false); |
| } |
| getTomcatInstance().start(); |
| |
| String digestUri; |
| if (breakUri) { |
| digestUri = "/broken" + uri; |
| } else { |
| digestUri = uri; |
| } |
| List<String> auth = new ArrayList<>(); |
| auth.add(buildDigestResponse(user, pwd, digestUri, realm, "null", |
| "null", nc1, cnonce, qop)); |
| Map<String,List<String>> reqHeaders = new HashMap<>(); |
| reqHeaders.put(CLIENT_AUTH_HEADER, auth); |
| |
| Map<String,List<String>> respHeaders = new HashMap<>(); |
| |
| // The first request will fail - but we need to extract the nonce |
| ByteChunk bc = new ByteChunk(); |
| int rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, |
| respHeaders); |
| assertEquals(401, rc); |
| assertTrue(bc.getLength() > 0); |
| bc.recycle(); |
| |
| // Second request should succeed (if we use the server nonce) |
| auth.clear(); |
| if (useServerNonce) { |
| if (useServerOpaque) { |
| auth.add(buildDigestResponse(user, pwd, digestUri, realm, |
| getNonce(respHeaders), getOpaque(respHeaders), nc1, |
| cnonce, qop)); |
| } else { |
| auth.add(buildDigestResponse(user, pwd, digestUri, realm, |
| getNonce(respHeaders), "null", nc1, cnonce, qop)); |
| } |
| } else { |
| auth.add(buildDigestResponse(user, pwd, digestUri, realm, |
| "null", getOpaque(respHeaders), nc1, cnonce, QOP)); |
| } |
| rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, |
| null); |
| |
| if (req2expect200) { |
| assertEquals(200, rc); |
| assertEquals("OK", bc.toString()); |
| } else { |
| assertEquals(401, rc); |
| assertTrue(bc.getLength() > 0); |
| } |
| |
| // Third request should succeed if we increment nc |
| auth.clear(); |
| bc.recycle(); |
| bc.reset(); |
| auth.add(buildDigestResponse(user, pwd, digestUri, realm, |
| getNonce(respHeaders), getOpaque(respHeaders), nc2, cnonce, |
| qop)); |
| rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, |
| null); |
| |
| if (req3expect200) { |
| assertEquals(200, rc); |
| assertEquals("OK", bc.toString()); |
| } else { |
| assertEquals(401, rc); |
| assertTrue(bc.getLength() > 0); |
| } |
| } |
| |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| |
| // Configure a context with digest auth and a single protected resource |
| Tomcat tomcat = getTomcatInstance(); |
| |
| // No file system docBase required |
| Context ctxt = tomcat.addContext(CONTEXT_PATH, null); |
| |
| // Add protected servlet |
| Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); |
| ctxt.addServletMappingDecoded(URI, "TesterServlet"); |
| SecurityCollection collection = new SecurityCollection(); |
| collection.addPatternDecoded(URI); |
| SecurityConstraint sc = new SecurityConstraint(); |
| sc.addAuthRole(ROLE); |
| sc.addCollection(collection); |
| ctxt.addConstraint(sc); |
| |
| // Configure the Realm |
| TesterMapRealm realm = new TesterMapRealm(); |
| realm.addUser(USER, PWD); |
| realm.addUserRole(USER, ROLE); |
| ctxt.setRealm(realm); |
| |
| // Configure the authenticator |
| LoginConfig lc = new LoginConfig(); |
| lc.setAuthMethod("DIGEST"); |
| lc.setRealmName(REALM); |
| ctxt.setLoginConfig(lc); |
| ctxt.getPipeline().addValve(new DigestAuthenticator()); |
| } |
| |
| protected static String getNonce(Map<String,List<String>> respHeaders) { |
| List<String> authHeaders = |
| respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME); |
| // Assume there is only one |
| String authHeader = authHeaders.iterator().next(); |
| |
| int start = authHeader.indexOf("nonce=\"") + 7; |
| int end = authHeader.indexOf('\"', start); |
| return authHeader.substring(start, end); |
| } |
| |
| protected static String getOpaque(Map<String,List<String>> respHeaders) { |
| List<String> authHeaders = |
| respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME); |
| // Assume there is only one |
| String authHeader = authHeaders.iterator().next(); |
| |
| int start = authHeader.indexOf("opaque=\"") + 8; |
| int end = authHeader.indexOf('\"', start); |
| return authHeader.substring(start, end); |
| } |
| |
| /* |
| * Notes from RFC2617 |
| * H(data) = MD5(data) |
| * KD(secret, data) = H(concat(secret, ":", data)) |
| * A1 = unq(username-value) ":" unq(realm-value) ":" passwd |
| * A2 = Method ":" digest-uri-value |
| * request-digest = <"> < KD ( H(A1), unq(nonce-value) |
| ":" nc-value |
| ":" unq(cnonce-value) |
| ":" unq(qop-value) |
| ":" H(A2) |
| ) <"> |
| */ |
| private static String buildDigestResponse(String user, String pwd, |
| String uri, String realm, String nonce, String opaque, String nc, |
| String cnonce, String qop) { |
| |
| String a1 = user + ":" + realm + ":" + pwd; |
| String a2 = "GET:" + uri; |
| |
| String md5a1 = digest(a1); |
| String md5a2 = digest(a2); |
| |
| String response; |
| if (qop == null) { |
| response = md5a1 + ":" + nonce + ":" + md5a2; |
| } else { |
| response = md5a1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + |
| qop + ":" + md5a2; |
| } |
| |
| String md5response = digest(response); |
| |
| StringBuilder auth = new StringBuilder(); |
| auth.append("Digest username=\""); |
| auth.append(user); |
| auth.append("\", realm=\""); |
| auth.append(realm); |
| auth.append("\", nonce=\""); |
| auth.append(nonce); |
| auth.append("\", uri=\""); |
| auth.append(uri); |
| auth.append("\", opaque=\""); |
| auth.append(opaque); |
| auth.append("\", response=\""); |
| auth.append(md5response); |
| auth.append("\""); |
| if (qop != null) { |
| auth.append(", qop="); |
| auth.append(qop); |
| auth.append(""); |
| } |
| if (nc != null) { |
| auth.append(", nc="); |
| auth.append(nc); |
| } |
| if (cnonce != null) { |
| auth.append(", cnonce=\""); |
| auth.append(cnonce); |
| auth.append("\""); |
| } |
| |
| return auth.toString(); |
| } |
| |
| private static String digest(String input) { |
| return MD5Encoder.encode( |
| ConcurrentMessageDigest.digestMD5(input.getBytes())); |
| } |
| |
| |
| private static class TesterRequest extends Request { |
| |
| @Override |
| public String getRemoteAddr() { |
| return "127.0.0.1"; |
| } |
| } |
| } |