/* * 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.realm; import java.lang.reflect.Field; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.BasicAttributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.naming.NameParserImpl; import org.apache.tomcat.unittest.TesterContext; import org.apache.tomcat.util.security.MD5Encoder; import org.easymock.EasyMock; public class TestJNDIRealm { private static final String ALGORITHM = "MD5"; private static final String USER = "test-user"; private static final String PASSWORD = "test-password"; private static final String REALM = "test-realm"; private static final String NONCE = "test-nonce"; private static final String HA2 = "test-md5a2"; public static final String USER_PASSWORD_ATTR = "test-pwd"; private static MessageDigest md5Helper; @BeforeClass public static void setupClass() throws Exception { md5Helper = MessageDigest.getInstance(ALGORITHM); } @Test public void testAuthenticateWithoutUserPassword() throws Exception { // GIVEN JNDIRealm realm = buildRealm(PASSWORD); // WHEN String expectedResponse = MD5Encoder.encode(md5Helper.digest((ha1() + ":" + NONCE + ":" + HA2).getBytes())); Principal principal = realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, HA2); // THEN Assert.assertNull(principal); } @Test public void testAuthenticateWithUserPassword() throws Exception { // GIVEN JNDIRealm realm = buildRealm(PASSWORD); realm.setUserPassword(USER_PASSWORD_ATTR); // WHEN String expectedResponse = MD5Encoder.encode(md5Helper.digest((ha1() + ":" + NONCE + ":" + HA2).getBytes())); Principal principal = realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, HA2); // THEN Assert.assertTrue(principal instanceof GenericPrincipal); Assert.assertEquals(PASSWORD, ((GenericPrincipal)principal).getPassword()); } @Test public void testAuthenticateWithUserPasswordAndCredentialHandler() throws Exception { // GIVEN JNDIRealm realm = buildRealm(ha1()); realm.setCredentialHandler(buildCredentialHandler()); realm.setUserPassword(USER_PASSWORD_ATTR); // WHEN String expectedResponse = MD5Encoder.encode(md5Helper.digest((ha1() + ":" + NONCE + ":" + HA2).getBytes())); Principal principal = realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, HA2); // THEN Assert.assertTrue(principal instanceof GenericPrincipal); Assert.assertEquals(ha1(), ((GenericPrincipal)principal).getPassword()); } private JNDIRealm buildRealm(String password) throws javax.naming.NamingException, NoSuchFieldException, IllegalAccessException, LifecycleException { Context context = new TesterContext(); JNDIRealm realm = new JNDIRealm(); realm.setContainer(context); realm.setUserSearch(""); Field field = JNDIRealm.class.getDeclaredField("context"); field.setAccessible(true); field.set(realm, mockDirContext(mockSearchResults(password))); realm.start(); return realm; } private MessageDigestCredentialHandler buildCredentialHandler() throws NoSuchAlgorithmException { MessageDigestCredentialHandler credentialHandler = new MessageDigestCredentialHandler(); credentialHandler.setAlgorithm(ALGORITHM); return credentialHandler; } private NamingEnumeration<SearchResult> mockSearchResults(String password) throws NamingException { @SuppressWarnings("unchecked") NamingEnumeration<SearchResult> searchResults = EasyMock.createNiceMock(NamingEnumeration.class); EasyMock.expect(Boolean.valueOf(searchResults.hasMore())) .andReturn(Boolean.TRUE) .andReturn(Boolean.FALSE) .andReturn(Boolean.TRUE) .andReturn(Boolean.FALSE); EasyMock.expect(searchResults.next()) .andReturn(new SearchResult("ANY RESULT", "", new BasicAttributes(USER_PASSWORD_ATTR, password))) .times(2); EasyMock.replay(searchResults); return searchResults; } private DirContext mockDirContext(NamingEnumeration<SearchResult> namingEnumeration) throws NamingException { DirContext dirContext = EasyMock.createNiceMock(InitialDirContext.class); EasyMock.expect(dirContext.search(EasyMock.anyString(), EasyMock.anyString(), EasyMock.anyObject(SearchControls.class))) .andReturn(namingEnumeration) .times(2); EasyMock.expect(dirContext.getNameParser("")) .andReturn(new NameParserImpl()).times(2); EasyMock.expect(dirContext.getNameInNamespace()) .andReturn("ANY NAME") .times(2); EasyMock.replay(dirContext); return dirContext; } private String ha1() { String a1 = USER + ":" + REALM + ":" + PASSWORD; return MD5Encoder.encode(md5Helper.digest(a1.getBytes())); } }