Author: markt Date: Fri Jun 26 08:07:37 2015 New Revision: 1687710 URL: http://svn.apache.org/r1687710 Log: Ported test to validate JASPIC DIGEST implementation Patch by fjodorver
Added: tomcat/trunk/test/org/apache/catalina/authenticator/TestJaspicDigestAuthenticator.java (with props) Modified: tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java Modified: tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java?rev=1687710&r1=1687709&r2=1687710&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java (original) +++ tomcat/trunk/java/org/apache/catalina/authenticator/jaspic/provider/modules/DigestAuthModule.java Fri Jun 26 08:07:37 2015 @@ -238,18 +238,13 @@ public class DigestAuthModule extends To DigestInfo digestInfo = new DigestInfo(getOpaque(), getNonceValidity(), getKey(), nonces, isValidateUri(), getRealmName()); - if (authorization == null) { + if (authorization == null || !digestInfo.parse(request, authorization)) { String nonce = generateNonce(request); - String authenticateHeader = getAuthenticateHeader(nonce, false); return sendUnauthorizedError(response, authenticateHeader); } - if (!digestInfo.parse(request, authorization)) { - return AuthStatus.SEND_FAILURE; - } - if (digestInfo.validate(request)) { principal = digestInfo.authenticate(realm); } Added: tomcat/trunk/test/org/apache/catalina/authenticator/TestJaspicDigestAuthenticator.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/authenticator/TestJaspicDigestAuthenticator.java?rev=1687710&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/catalina/authenticator/TestJaspicDigestAuthenticator.java (added) +++ tomcat/trunk/test/org/apache/catalina/authenticator/TestJaspicDigestAuthenticator.java Fri Jun 26 08:07:37 2015 @@ -0,0 +1,410 @@ +/* + * 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 javax.security.auth.message.config.AuthConfigFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.authenticator.jaspic.JaspicAuthenticator; +import org.apache.catalina.authenticator.jaspic.provider.TomcatAuthConfigProvider; +import org.apache.catalina.connector.Request; +import org.apache.catalina.core.TesterContext; +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.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 TestJaspicDigestAuthenticator 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 + @Ignore("URI validation is not implemented yet") + 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.addServletMapping(URI, "TesterServlet"); + SecurityCollection collection = new SecurityCollection(); + collection.addPattern(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); + + AuthConfigFactory authConfigFactory = AuthConfigFactory.getFactory(); + TomcatAuthConfigProvider provider = new TomcatAuthConfigProvider(ctxt); + authConfigFactory.registerConfigProvider(provider, JaspicAuthenticator.MESSAGE_LAYER, + null, "Tomcat Jaspic"); + ctxt.getPipeline().addValve(new JaspicAuthenticator()); + } + + + 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"; + } + } +} Propchange: tomcat/trunk/test/org/apache/catalina/authenticator/TestJaspicDigestAuthenticator.java ------------------------------------------------------------------------------ svn:eol-style = native --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org