This commit is contained in:
2024-11-30 19:03:49 +08:00
commit 1e6763c160
3806 changed files with 737676 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
/*
* 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.List;
import java.util.Map;
/**
* This class incorporates test response data
*/
class ResponseDescriptor {
private Map<String, List<String>> headers;
private String body;
private int responseCode;
public Map<String, List<String>> getHeaders() {
return headers;
}
public void setHeaders(Map<String, List<String>> headers) {
this.headers = headers;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public int getResponseCode() {
return responseCode;
}
public void setResponseCode(int responseCode) {
this.responseCode = responseCode;
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
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.catalina.valves.RemoteIpValve;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.codec.binary.Base64;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
public class TestAuthInfoResponseHeaders 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 CONTEXT_PATH = "/foo";
private static String CLIENT_AUTH_HEADER = "authorization";
/*
* Encapsulate the logic to generate an HTTP header
* for BASIC Authentication.
* Note: only used internally, so no need to validate arguments.
*/
private static final class BasicCredentials {
private final String method;
private final String username;
private final String password;
private final String credentials;
private BasicCredentials(String aMethod,
String aUsername, String aPassword) {
method = aMethod;
username = aUsername;
password = aPassword;
String userCredentials = username + ":" + password;
byte[] credentialsBytes =
userCredentials.getBytes(StandardCharsets.ISO_8859_1);
String base64auth = Base64.encodeBase64String(credentialsBytes);
credentials= method + " " + base64auth;
}
private String getCredentials() {
return credentials;
}
}
@Test
public void testNoHeaders() throws Exception {
doTest(USER, PWD, CONTEXT_PATH + URI, false);
}
@Test
public void testWithHeaders() throws Exception {
doTest(USER, PWD, CONTEXT_PATH + URI, true);
}
public void doTest(String user, String pwd, String uri, boolean expectResponseAuthHeaders)
throws Exception {
if (expectResponseAuthHeaders) {
BasicAuthenticator auth =
(BasicAuthenticator) getTomcatInstance().getHost().findChild(
CONTEXT_PATH).getPipeline().getFirst();
auth.setSendAuthInfoResponseHeaders(true);
}
getTomcatInstance().start();
Map<String,List<String>> reqHeaders = new HashMap<>();
List<String> auth = new ArrayList<>();
auth.add(new BasicCredentials("Basic", user, pwd).getCredentials());
reqHeaders.put(CLIENT_AUTH_HEADER, auth);
List<String> forwardedFor = new ArrayList<>();
forwardedFor.add("192.168.0.10");
List<String> forwardedHost = new ArrayList<>();
forwardedHost.add("localhost");
reqHeaders.put("X-Forwarded-For", forwardedFor);
reqHeaders.put("X-Forwarded-Host", forwardedHost);
Map<String,List<String>> respHeaders = new HashMap<>();
ByteChunk bc = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders,
respHeaders);
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
if (expectResponseAuthHeaders) {
List<String> remoteUsers = respHeaders.get("remote-user");
Assert.assertNotNull(remoteUsers);
Assert.assertEquals(USER, remoteUsers.get(0));
List<String> authTypes = respHeaders.get("auth-type");
Assert.assertNotNull(authTypes);
Assert.assertEquals(HttpServletRequest.BASIC_AUTH, authTypes.get(0));
} else {
Assert.assertFalse(respHeaders.containsKey("remote-user"));
Assert.assertFalse(respHeaders.containsKey("auth-type"));
}
bc.recycle();
}
@Override
public void setUp() throws Exception {
super.setUp();
// Configure a context with digest auth and a single protected resource
Tomcat tomcat = getTomcatInstance();
tomcat.getHost().getPipeline().addValve(new RemoteIpValve());
// 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(HttpServletRequest.BASIC_AUTH);
ctxt.setLoginConfig(lc);
ctxt.getPipeline().addValve(new BasicAuthenticator());
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.apache.catalina.Context;
import org.apache.catalina.Realm;
import org.apache.catalina.authenticator.AuthenticatorBase.AllowCorsPreflight;
import org.apache.catalina.filters.AddDefaultCharsetFilter;
import org.apache.catalina.filters.CorsFilter;
import org.apache.catalina.realm.NullRealm;
import org.apache.catalina.servlets.DefaultServlet;
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.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
@RunWith(Parameterized.class)
public class TestAuthenticatorBaseCorsPreflight extends TomcatBaseTest {
private static final String ALLOWED_ORIGIN = "http://example.com";
private static final String EMPTY_ORIGIN = "";
private static final String INVALID_ORIGIN = "http://%20";
private static final String SAME_ORIGIN = "http://localhost";
private static final String ALLOWED_METHOD = "GET";
private static final String BLOCKED_METHOD = "POST";
private static final String EMPTY_METHOD = "";
@Parameterized.Parameters(name = "{index}: input[{0}]")
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
parameterSets.add(new Object[] { AllowCorsPreflight.NEVER, "/*", "OPTIONS", null, null, Boolean.FALSE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", null, null, Boolean.FALSE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.TRUE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", EMPTY_ORIGIN, ALLOWED_METHOD, Boolean.FALSE});
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", INVALID_ORIGIN, ALLOWED_METHOD, Boolean.FALSE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", SAME_ORIGIN, ALLOWED_METHOD, Boolean.FALSE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "GET", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.FALSE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, BLOCKED_METHOD, Boolean.FALSE });
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, EMPTY_METHOD, Boolean.FALSE});
parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, null, Boolean.FALSE});
parameterSets.add(new Object[] { AllowCorsPreflight.FILTER, "/*", "OPTIONS", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.TRUE });
parameterSets.add(new Object[] { AllowCorsPreflight.FILTER, "/x", "OPTIONS", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.FALSE });
return parameterSets;
}
@Parameter(0)
public AllowCorsPreflight allowCorsPreflight;
@Parameter(1)
public String filterMapping;
@Parameter(2)
public String method;
@Parameter(3)
public String origin;
@Parameter(4)
public String accessControl;
@Parameter(5)
public boolean allow;
@BeforeClass
public static void init() {
// So the test can set the origin header
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
}
@Test
public void test() throws Exception {
Tomcat tomcat = getTomcatInstance();
File appDir = new File("test/webapp");
Context ctx = tomcat.addContext("", appDir.getAbsolutePath());
Tomcat.addServlet(ctx, "default", new DefaultServlet());
ctx.addServletMappingDecoded("/", "default");
LoginConfig loginConfig = new LoginConfig();
loginConfig.setAuthMethod("BASIC");
ctx.setLoginConfig(loginConfig);
BasicAuthenticator basicAuth = new BasicAuthenticator();
basicAuth.setAllowCorsPreflight(allowCorsPreflight.toString());
ctx.getPipeline().addValve(basicAuth);
Realm realm = new NullRealm();
ctx.setRealm(realm);
SecurityCollection securityCollection = new SecurityCollection();
securityCollection.addPattern("/*");
SecurityConstraint constraint = new SecurityConstraint();
constraint.setAuthConstraint(true);
constraint.addCollection(securityCollection);
ctx.addConstraint(constraint);
// For code coverage
FilterDef otherFilter = new FilterDef();
otherFilter.setFilterName("other");
otherFilter.setFilterClass(AddDefaultCharsetFilter.class.getName());
FilterMap otherMap = new FilterMap();
otherMap.setFilterName("other");
otherMap.addURLPatternDecoded("/other");
ctx.addFilterDef(otherFilter);
ctx.addFilterMap(otherMap);
FilterDef corsFilter = new FilterDef();
corsFilter.setFilterName("cors");
corsFilter.setFilterClass(CorsFilter.class.getName());
corsFilter.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_ORIGINS, ALLOWED_ORIGIN);
corsFilter.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_METHODS, ALLOWED_METHOD);
FilterMap corsFilterMap = new FilterMap();
corsFilterMap.setFilterName("cors");
corsFilterMap.addURLPatternDecoded(filterMapping);
ctx.addFilterDef(corsFilter);
ctx.addFilterMap(corsFilterMap);
tomcat.start();
Map<String,List<String>> reqHead = new HashMap<>();
if (origin != null) {
List<String> values = new ArrayList<>();
if (SAME_ORIGIN.equals(origin)) {
values.add(origin + ":" + getPort());
} else {
values.add(origin);
}
reqHead.put(CorsFilter.REQUEST_HEADER_ORIGIN, values);
}
if (accessControl != null) {
List<String> values = new ArrayList<>();
values.add(accessControl);
reqHead.put(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, values);
}
ByteChunk out = new ByteChunk();
int rc = methodUrl("http://localhost:" + getPort() + "/target", out, 300000, reqHead, null, method, false);
if (allow) {
Assert.assertEquals(200, rc);
} else {
Assert.assertEquals(403, rc);
}
}
}

View File

@@ -0,0 +1,558 @@
/*
* 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.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.Assert;
import org.junit.Test;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.codec.binary.Base64;
/**
* Test the BasicAuthenticator's BasicCredentials inner class and the
* associated Base64 decoder.
*/
public class TestBasicAuthParser {
private static final String NICE_METHOD = "Basic";
private static final String USER_NAME = "userid";
private static final String PASSWORD = "secret";
/*
* test cases with good BASIC Auth credentials - Base64 strings
* can have zero, one or two trailing pad characters
*/
@Test
public void testGoodCredentials() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testGoodCredentialsNoPassword() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, null);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNull(credentials.getPassword());
}
@Test
public void testGoodCrib() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA==";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testGoodCribUserOnly() throws Exception {
final String BASE64_CRIB = "dXNlcmlk";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNull(credentials.getPassword());
}
@Test
public void testGoodCribOnePad() throws Exception {
final String PASSWORD1 = "secrets";
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldHM=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD1, credentials.getPassword());
}
/*
* RFC 2045 says the Base64 encoded string should be represented
* as lines of no more than 76 characters. However, RFC 2617
* says a base64-user-pass token is not limited to 76 char/line.
* It also says all line breaks, including mandatory ones,
* should be ignored during decoding.
* This test case has a line break in the Base64 string.
* (See also testGoodCribBase64Big below).
*/
@Test
public void testGoodCribLineWrap() throws Exception {
final String USER_LONG = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz0123456789+/AAAABBBBCCCC"
+ "DDDD"; // 80 characters
final String BASE64_CRIB = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldY"
+ "WVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0"
+ "\n" + "NTY3ODkrL0FBQUFCQkJCQ0NDQ0REREQ=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_LONG, credentials.getUsername());
}
/*
* RFC 2045 says the Base64 encoded string should be represented
* as lines of no more than 76 characters. However, RFC 2617
* says a base64-user-pass token is not limited to 76 char/line.
*/
@Test
public void testGoodCribBase64Big() throws Exception {
// Our decoder accepts a long token without complaint.
final String USER_LONG = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz0123456789+/AAAABBBBCCCC"
+ "DDDD"; // 80 characters
final String BASE64_CRIB = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldY"
+ "WVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0"
+ "NTY3ODkrL0FBQUFCQkJCQ0NDQ0REREQ="; // no new line
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_LONG, credentials.getUsername());
}
/*
* verify the parser follows RFC2617 by treating the auth-scheme
* token as case-insensitive.
*/
@Test
public void testAuthMethodCaseBasic() throws Exception {
final String METHOD = "bAsIc";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(METHOD, USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Confirm the Basic parser rejects an invalid authentication method.
*/
@Test
public void testAuthMethodBadMethod() throws Exception {
final String METHOD = "BadMethod";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(METHOD, USER_NAME, PASSWORD);
@SuppressWarnings("unused")
BasicAuthenticator.BasicCredentials credentials = null;
try {
credentials = new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.fail("IllegalArgumentException expected");
}
catch (Exception e) {
Assert.assertTrue(e instanceof IllegalArgumentException);
Assert.assertTrue(e.getMessage().contains("header method"));
}
}
/*
* Confirm the Basic parser tolerates excess white space after
* the authentication method.
*
* RFC2617 does not define the separation syntax between the auth-scheme
* and basic-credentials tokens. Tomcat tolerates any amount of white
* (within the limits of HTTP header sizes).
*/
@Test
public void testAuthMethodExtraLeadingSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD + " ", USER_NAME, PASSWORD);
final BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* invalid decoded credentials cases
*/
@Test
public void testWrongPassword() throws Exception {
final String PWD_WRONG = "wrong";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PWD_WRONG);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertNotSame(PASSWORD, credentials.getPassword());
}
@Test
public void testMissingUsername() throws Exception {
final String EMPTY_USER_NAME = "";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, EMPTY_USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(EMPTY_USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testShortUsername() throws Exception {
final String SHORT_USER_NAME = "a";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, SHORT_USER_NAME, PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(SHORT_USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
@Test
public void testShortPassword() throws Exception {
final String SHORT_PASSWORD = "a";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, SHORT_PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(SHORT_PASSWORD, credentials.getPassword());
}
@Test
public void testPasswordHasSpaceEmbedded() throws Exception {
final String PASSWORD_SPACE = "abc def";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_SPACE);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_SPACE, credentials.getPassword());
}
@Test
public void testPasswordHasColonEmbedded() throws Exception {
final String PASSWORD_COLON = "abc:def";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_COLON, credentials.getPassword());
}
@Test
public void testPasswordHasColonLeading() throws Exception {
final String PASSWORD_COLON = ":abcdef";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_COLON, credentials.getPassword());
}
@Test
public void testPasswordHasColonTrailing() throws Exception {
final String PASSWORD_COLON = "abcdef:";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD_COLON, credentials.getPassword());
}
/*
* Confirm the Basic parser tolerates excess white space after
* the base64 blob.
*
* RFC2617 does not define this case, but asks servers to be
* tolerant of this kind of client deviation.
*/
@Test
public void testAuthMethodExtraTrailingSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD, " ");
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Confirm the Basic parser tolerates excess white space around
* the username inside the base64 blob.
*
* RFC2617 does not define the separation syntax between the auth-scheme
* and basic-credentials tokens. Tomcat should tolerate any reasonable
* amount of white space.
*/
@Test
public void testUserExtraSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, " " + USER_NAME + " ", PASSWORD);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Confirm the Basic parser tolerates excess white space around
* the username within the base64 blob.
*
* RFC2617 does not define the separation syntax between the auth-scheme
* and basic-credentials tokens. Tomcat should tolerate any reasonable
* amount of white space.
*/
@Test
public void testPasswordExtraSpace() throws Exception {
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, USER_NAME, " " + PASSWORD + " ");
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* invalid base64 string tests
*
* Refer to
* - RFC 7617 (Basic Auth)
* - RFC 4648 (base 64)
*/
/*
* non-trailing "=" is illegal and will be rejected by the parser
*/
@Test(expected = IllegalArgumentException.class)
public void testBadBase64InlineEquals() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3J=dAo=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
@SuppressWarnings("unused") // Exception will be thrown.
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
}
/*
* "-" is not a legal base64 character. The RFC says it must be
* ignored by the decoder. This will scramble the decoded string
* and eventually result in an IllegalArgumentException.
*/
@Test(expected = IllegalArgumentException.class)
public void testBadBase64Char() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNl-3JldHM=";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
@SuppressWarnings("unused")
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
}
/*
* "-" is not a legal base64 character. The RFC says it must be
* ignored by the decoder. This is a very strange case because the
* next character is a pad, which terminates the string normally.
* It is likely (but not certain) the decoded password will be
* damaged and subsequent authentication will fail.
*/
@Test
public void testBadBase64LastChar() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA-=";
final String POSSIBLY_DAMAGED_PWD = "secret";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(POSSIBLY_DAMAGED_PWD, credentials.getPassword());
}
/*
* The trailing third "=" is illegal. However, the RFC says the decoder
* must terminate as soon as the first pad is detected, so no error
* will be detected unless the payload has been damaged in some way.
*/
@Test
public void testBadBase64TooManyEquals() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA===";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* there should be a multiple of 4 encoded characters. However,
* the RFC says the decoder should pad the input string with
* zero bits out to the next boundary. An error will not be detected
* unless the payload has been damaged in some way - this
* particular crib has no damage.
*/
@Test
public void testBadBase64BadLength() throws Exception {
final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA";
final BasicAuthHeader AUTH_HEADER =
new BasicAuthHeader(NICE_METHOD, BASE64_CRIB);
BasicAuthenticator.BasicCredentials credentials =
new BasicAuthenticator.BasicCredentials(
AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true);
Assert.assertEquals(USER_NAME, credentials.getUsername());
Assert.assertEquals(PASSWORD, credentials.getPassword());
}
/*
* Encapsulate the logic to generate an HTTP header
* for BASIC Authentication.
* Note: only used internally, so no need to validate arguments.
*/
private final class BasicAuthHeader {
private final String HTTP_AUTH = "authorization: ";
private final byte[] HEADER =
HTTP_AUTH.getBytes(StandardCharsets.ISO_8859_1);
private ByteChunk authHeader;
private int initialOffset = 0;
/*
* This method creates a valid base64 blob
*/
private BasicAuthHeader(String method, String username,
String password) {
this(method, username, password, null);
}
/*
* This method creates valid base64 blobs with optional trailing data
*/
private BasicAuthHeader(String method, String username,
String password, String extraBlob) {
prefix(method);
String userCredentials =
((password == null) || (password.length() < 1))
? username
: username + ":" + password;
byte[] credentialsBytes =
userCredentials.getBytes(StandardCharsets.ISO_8859_1);
String base64auth = Base64.encodeBase64String(credentialsBytes);
byte[] base64Bytes =
base64auth.getBytes(StandardCharsets.ISO_8859_1);
byte[] extraBytes =
((extraBlob == null) || (extraBlob.length() < 1))
? null :
extraBlob.getBytes(StandardCharsets.ISO_8859_1);
try {
authHeader.append(base64Bytes, 0, base64Bytes.length);
if (extraBytes != null) {
authHeader.append(extraBytes, 0, extraBytes.length);
}
}
catch (IOException ioe) {
throw new IllegalStateException("unable to extend ByteChunk:"
+ ioe.getMessage());
}
// emulate tomcat server - offset points to method in header
authHeader.setOffset(initialOffset);
}
/*
* This method allows injection of cribbed base64 blobs,
* without any validation of the contents
*/
private BasicAuthHeader(String method, String fakeBase64) {
prefix(method);
byte[] fakeBytes = fakeBase64.getBytes(StandardCharsets.ISO_8859_1);
try {
authHeader.append(fakeBytes, 0, fakeBytes.length);
}
catch (IOException ioe) {
throw new IllegalStateException("unable to extend ByteChunk:"
+ ioe.getMessage());
}
// emulate tomcat server - offset points to method in header
authHeader.setOffset(initialOffset);
}
/*
* construct the common authorization header
*/
private void prefix(String method) {
authHeader = new ByteChunk();
authHeader.setBytes(HEADER, 0, HEADER.length);
initialOffset = HEADER.length;
String methodX = method + " ";
byte[] methodBytes = methodX.getBytes(StandardCharsets.ISO_8859_1);
try {
authHeader.append(methodBytes, 0, methodBytes.length);
}
catch (IOException ioe) {
throw new IllegalStateException("unable to extend ByteChunk:"
+ ioe.getMessage());
}
}
private ByteChunk getHeader() {
return authHeader;
}
}
}

View File

@@ -0,0 +1,397 @@
/*
* 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 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.unittest.TesterServletContext;
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();
TesterContext context = new TesterContext();
context.setServletContext(new TesterServletContext());
digestAuthenticator.setContainer(context);
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);
Assert.assertEquals(401, rc);
Assert.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) {
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
} else {
Assert.assertEquals(401, rc);
Assert.assertTrue(bc.getLength() > 0);
}
// Third request should succeed if we increment nc
auth.clear();
bc.recycle();
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) {
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
} else {
Assert.assertEquals(401, rc);
Assert.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";
}
}
}

View File

@@ -0,0 +1,794 @@
/*
* 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.io.File;
import java.io.IOException;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Valve;
import org.apache.catalina.startup.SimpleHttpClient;
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.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.websocket.server.WsContextListener;
/*
* Test FORM authentication for sessions that do and do not use cookies.
*
* 1. A client that can accept and respond to a Set-Cookie for JSESSIONID
* will be able to maintain its authenticated session, no matter whether
* the session ID is changed once, many times, or not at all.
*
* 2. A client that cannot accept cookies will only be able to maintain a
* persistent session IF the server sends the correct (current) jsessionid
* as a path parameter appended to ALL urls within its response. That is
* achievable with servlets, jsps, jstl (all of which which can ask for an
* encoded url to be inserted into the dynamic web page). It cannot work
* with static html.
* note: this test class uses the Tomcat sample jsps, which conform.
*
* 3. Therefore, any webapp that MIGHT need to authenticate a client that
* does not accept cookies MUST generate EVERY protected resource url
* dynamically (so that it will include the current session ID).
*
* 4. Any webapp that cannot satisfy case 3 MUST turn off
* changeSessionIdOnAuthentication for its Context and thus degrade the
* session fixation protection for ALL of its clients.
* note from MarkT: Not sure I agree with this. If the URLs aren't
* being encoded, then the session is going to break regardless of
* whether or not the session ID changes.
*
* Unlike a "proper browser", this unit test class does a quite lot of
* screen-scraping and cheating of headers and urls (not very elegant,
* but it makes no claims to generality).
*
*/
public class TestFormAuthenticator extends TomcatBaseTest {
// these should really be singletons to be type-safe,
// we are in a unit test and don't need to paranoid.
protected static final boolean USE_100_CONTINUE = true;
protected static final boolean NO_100_CONTINUE = !USE_100_CONTINUE;
protected static final boolean CLIENT_USE_COOKIES = true;
protected static final boolean CLIENT_NO_COOKIES = !CLIENT_USE_COOKIES;
protected static final boolean CLIENT_USE_HTTP_11 = true;
protected static final boolean CLIENT_USE_HTTP_10 = !CLIENT_USE_HTTP_11;
protected static final boolean SERVER_USE_COOKIES = true;
protected static final boolean SERVER_NO_COOKIES = !SERVER_USE_COOKIES;
protected static final boolean SERVER_CHANGE_SESSID = true;
protected static final boolean SERVER_FREEZE_SESSID = !SERVER_CHANGE_SESSID;
// minimum session timeout
private static final int SHORT_SESSION_TIMEOUT_SECS = 1;
private static final long TIMEOUT_DELAY_MSECS = ((SHORT_SESSION_TIMEOUT_SECS + 10) * 1000);
private FormAuthClient client;
// first, a set of tests where the server uses a cookie to carry
// the current session ID during and after authentication, and
// the client is prepared to return cookies with each request
@Test
public void testGetWithCookies() throws Exception {
doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
@Test
public void testPostNoContinueWithCookies() throws Exception {
doTest("POST", "GET", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
@Test
public void testPostWithContinueAndCookies() throws Exception {
doTest("POST", "GET", USE_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
// Bug 49779
@Test
public void testPostNoContinuePostRedirectWithCookies() throws Exception {
doTest("POST", "POST", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
// Bug 49779
@Test
public void testPostWithContinuePostRedirectWithCookies() throws Exception {
doTest("POST", "POST", USE_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
// next, a set of tests where the server Context is configured to never
// use cookies and the session ID is only carried as a url path parameter
// Bug 53584
@Test
public void testGetNoServerCookies() throws Exception {
doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID);
}
@Test
public void testPostNoContinueNoServerCookies() throws Exception {
doTest("POST", "GET", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID);
}
@Test
public void testPostWithContinueNoServerCookies() throws Exception {
doTest("POST", "GET", USE_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID);
}
// variant of Bug 49779
@Test
public void testPostNoContinuePostRedirectNoServerCookies()
throws Exception {
doTest("POST", "POST", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID);
}
// variant of Bug 49779
@Test
public void testPostWithContinuePostRedirectNoServerCookies()
throws Exception {
doTest("POST", "POST", USE_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID);
}
// next, a set of tests where the server Context uses cookies,
// but the client refuses to return them and tries to use
// the session ID if carried as a url path parameter
@Test
public void testGetNoClientCookies() throws Exception {
doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
@Test
public void testPostNoContinueNoClientCookies() throws Exception {
doTest("POST", "GET", NO_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
@Test
public void testPostWithContinueNoClientCookies() throws Exception {
doTest("POST", "GET", USE_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
// variant of Bug 49779
@Test
public void testPostNoContinuePostRedirectNoClientCookies()
throws Exception {
doTest("POST", "POST", NO_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
// variant of Bug 49779
@Test
public void testPostWithContinuePostRedirectNoClientCookies()
throws Exception {
doTest("POST", "POST", USE_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID);
}
// finally, a set of tests to explore quirky situations
// but there is not need to replicate all the scenarios above.
@Test
public void testNoChangedSessidWithCookies() throws Exception {
doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES,
SERVER_FREEZE_SESSID);
}
@Test
public void testNoChangedSessidWithoutCookies() throws Exception {
doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES,
SERVER_FREEZE_SESSID);
}
@Test
public void testTimeoutWithoutCookies() throws Exception {
String protectedUri = doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_NO_COOKIES, SERVER_USE_COOKIES,
SERVER_FREEZE_SESSID);
// Force session to expire one second from now
Context context = (Context) getTomcatInstance().getHost().findChildren()[0];
forceSessionMaxInactiveInterval(context, SHORT_SESSION_TIMEOUT_SECS);
// wait long enough for my session to expire
Thread.sleep(TIMEOUT_DELAY_MSECS);
// then try to continue using the expired session to get the
// protected resource once more.
// should get login challenge or timeout status 408
doTestProtected("GET", protectedUri, NO_100_CONTINUE,
FormAuthClient.LOGIN_REQUIRED, 1);
}
// HTTP 1.0 test
@Test
public void testGetWithCookiesHttp10() throws Exception {
doTest("GET", "GET", NO_100_CONTINUE,
CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID,
CLIENT_USE_HTTP_10);
}
@Test
public void doTestSelectedMethods() throws Exception {
FormAuthClientSelectedMethods client =
new FormAuthClientSelectedMethods(true, true, true, true);
// First request for protected resource gets the login page
client.doResourceRequest("PUT", true, "/test?" +
SelectedMethodsServlet.PARAM + "=" +
SelectedMethodsServlet.VALUE, null);
Assert.assertTrue(client.getResponseLine(), client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK());
String originalSessionId = client.getSessionId();
client.reset();
// Second request replies to the login challenge
client.doResourceRequest("POST", true, "/test/j_security_check",
FormAuthClientBase.LOGIN_REPLY);
Assert.assertTrue("login failed " + client.getResponseLine(),
client.isResponse303());
Assert.assertTrue(client.isResponseBodyOK());
String redirectUri = client.getRedirectUri();
client.reset();
// Third request - the login was successful so
// follow the redirect to the protected resource
client.doResourceRequest("GET", true, redirectUri, null);
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK());
String newSessionId = client.getSessionId();
Assert.assertTrue(!originalSessionId.equals(newSessionId));
client.reset();
}
/*
* Choreograph the steps of the test dialogue with the server
* 1. while not authenticated, try to access a protected resource
* 2. respond to the login challenge with good credentials
* 3. after successful login, follow the redirect to the original page
* 4. repeatedly access the protected resource to demonstrate
* persistence of the authenticated session
*
* @param resourceMethod HTTP method for accessing the protected resource
* @param redirectMethod HTTP method for the login FORM reply
* @param useContinue whether the HTTP client should expect a 100 Continue
* @param clientShouldUseCookies whether the client should send cookies
* @param serverWillUseCookies whether the server should send cookies
*
*/
private String doTest(String resourceMethod, String redirectMethod,
boolean useContinue, boolean clientShouldUseCookies,
boolean serverWillUseCookies, boolean serverWillChangeSessid)
throws Exception {
return doTest(resourceMethod, redirectMethod, useContinue,
clientShouldUseCookies, serverWillUseCookies,
serverWillChangeSessid, true);
}
private String doTest(String resourceMethod, String redirectMethod,
boolean useContinue, boolean clientShouldUseCookies,
boolean serverWillUseCookies, boolean serverWillChangeSessid,
boolean clientShouldUseHttp11) throws Exception {
client = new FormAuthClient(clientShouldUseCookies,
clientShouldUseHttp11, serverWillUseCookies,
serverWillChangeSessid);
// First request for protected resource gets the login page
client.setUseContinue(useContinue);
client.doResourceRequest(resourceMethod, false, null, null);
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK());
String loginUri = client.extractBodyUri(
FormAuthClient.LOGIN_PARAM_TAG,
FormAuthClient.LOGIN_RESOURCE);
String originalSessionId = null;
if (serverWillUseCookies && clientShouldUseCookies) {
originalSessionId = client.getSessionId();
}
else {
originalSessionId = client.extractPathSessionId(loginUri);
}
client.reset();
// Second request replies to the login challenge
client.setUseContinue(useContinue);
client.doLoginRequest(loginUri);
if (clientShouldUseHttp11) {
Assert.assertTrue("login failed " + client.getResponseLine(),
client.isResponse303());
} else {
Assert.assertTrue("login failed " + client.getResponseLine(),
client.isResponse302());
}
Assert.assertTrue(client.isResponseBodyOK());
String redirectUri = client.getRedirectUri();
client.reset();
// Third request - the login was successful so
// follow the redirect to the protected resource
client.doResourceRequest(redirectMethod, true, redirectUri, null);
if ("POST".equals(redirectMethod)) {
client.setUseContinue(useContinue);
}
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK());
String protectedUri = client.extractBodyUri(
FormAuthClient.RESOURCE_PARAM_TAG,
FormAuthClient.PROTECTED_RESOURCE);
String newSessionId = null;
if (serverWillUseCookies && clientShouldUseCookies) {
newSessionId = client.getSessionId();
}
else {
newSessionId = client.extractPathSessionId(protectedUri);
}
boolean sessionIdIsChanged = !(originalSessionId.equals(newSessionId));
Assert.assertTrue(sessionIdIsChanged == serverWillChangeSessid);
client.reset();
// Subsequent requests - keep accessing the protected resource
doTestProtected(resourceMethod, protectedUri, useContinue,
FormAuthClient.LOGIN_SUCCESSFUL, 5);
return protectedUri; // in case more requests will be issued
}
/*
* Repeatedly access the protected resource after the client has
* successfully logged-in to the webapp. The current session attributes
* will be used and cannot be changed.
* 3. after successful login, follow the redirect to the original page
* 4. repeatedly access the protected resource to demonstrate
* persistence of the authenticated session
*
* @param resourceMethod HTTP method for accessing the protected resource
* @param protectedUri to access (with or without sessionid)
* @param useContinue whether the HTTP client should expect a 100 Continue
* @param clientShouldUseCookies whether the client should send cookies
* @param serverWillUseCookies whether the server should send cookies
*
*/
private void doTestProtected(String resourceMethod, String protectedUri,
boolean useContinue, int phase, int repeatCount)
throws Exception {
// Subsequent requests - keep accessing the protected resource
for (int i = 0; i < repeatCount; i++) {
client.setUseContinue(useContinue);
client.doResourceRequest(resourceMethod, false, protectedUri, null);
Assert.assertTrue(client.isResponse200());
Assert.assertTrue(client.isResponseBodyOK(phase));
client.reset();
}
}
/*
* Encapsulate the logic needed to run a suitably-configured tomcat
* instance, send it an HTTP request and process the server response
*/
private abstract class FormAuthClientBase extends SimpleHttpClient {
protected static final String LOGIN_PARAM_TAG = "action=";
protected static final String LOGIN_RESOURCE = "j_security_check";
protected static final String LOGIN_REPLY =
"j_username=tomcat&j_password=tomcat";
protected static final String PROTECTED_RELATIVE_PATH =
"/examples/jsp/security/protected/";
protected static final String PROTECTED_RESOURCE = "index.jsp";
private static final String PROTECTED_RESOURCE_URL =
PROTECTED_RELATIVE_PATH + PROTECTED_RESOURCE;
protected static final String RESOURCE_PARAM_TAG = "href=";
private static final char PARAM_DELIM = '?';
// primitive tracking of the test phases to verify the HTML body
protected static final int LOGIN_REQUIRED = 1;
protected static final int REDIRECTING = 2;
protected static final int LOGIN_SUCCESSFUL = 3;
private int requestCount = 0;
// todo: forgot this change and making it up again!
protected final String SESSION_PARAMETER_START =
SESSION_PARAMETER_NAME + "=";
protected boolean clientShouldUseHttp11;
protected void doLoginRequest(String loginUri) throws Exception {
doResourceRequest("POST", true,
PROTECTED_RELATIVE_PATH + loginUri, LOGIN_REPLY);
}
/*
* Prepare the resource request HTTP headers and issue the request.
* Three kinds of uri are supported:
* 1. fully qualified uri.
* 2. minimal uri without webapp path.
* 3. null - use the default protected resource
* Cookies are sent if available and supported by the test. Otherwise, the
* caller is expected to have provided a session id as a path parameter.
*/
protected void doResourceRequest(String method, boolean isFullQualUri,
String resourceUri, String requestTail) throws Exception {
// build the HTTP request while assembling the uri
StringBuilder requestHead = new StringBuilder(128);
requestHead.append(method).append(" ");
if (isFullQualUri) {
requestHead.append(resourceUri);
}
else {
if (resourceUri == null) {
// the default relative url
requestHead.append(PROTECTED_RESOURCE_URL);
}
else {
requestHead.append(PROTECTED_RELATIVE_PATH)
.append(resourceUri);
}
if ("GET".equals(method)) {
requestHead.append("?role=bar");
}
}
if (clientShouldUseHttp11) {
requestHead.append(" HTTP/1.1").append(CRLF);
} else {
requestHead.append(" HTTP/1.0").append(CRLF);
}
// next, add the constant http headers
requestHead.append("Host: localhost").append(CRLF);
requestHead.append("Connection: close").append(CRLF);
// then any optional http headers
if (getUseContinue()) {
requestHead.append("Expect: 100-continue").append(CRLF);
}
if (getUseCookies()) {
String sessionId = getSessionId();
if (sessionId != null) {
requestHead.append("Cookie: ")
.append(SESSION_COOKIE_NAME)
.append("=").append(sessionId).append(CRLF);
}
}
// finally, for posts only, deal with the request content
if ("POST".equals(method)) {
if (requestTail == null) {
requestTail = "role=bar";
}
requestHead.append(
"Content-Type: application/x-www-form-urlencoded")
.append(CRLF);
// calculate post data length
String len = Integer.toString(requestTail.length());
requestHead.append("Content-length: ").append(len).append(CRLF);
}
// always put an empty line after the headers
requestHead.append(CRLF);
String request[] = new String[2];
request[0] = requestHead.toString();
request[1] = requestTail;
doRequest(request);
}
private void doRequest(String request[]) throws Exception {
setRequest(request);
connect();
processRequest();
disconnect();
requestCount++;
}
/*
* verify the server response html body is the page we expect,
* based on the dialogue position within doTest.
*/
@Override
public boolean isResponseBodyOK() {
return isResponseBodyOK(requestCount);
}
/*
* verify the server response html body is the page we expect,
* based on the dialogue position given by the caller.
*/
public boolean isResponseBodyOK(int testPhase) {
switch (testPhase) {
case LOGIN_REQUIRED:
// First request should return in the login page
assertContains(getResponseBody(),
"<title>Login Page for Examples</title>");
return true;
case REDIRECTING:
// Second request should result in redirect without a body
return true;
default:
// Subsequent requests should return in the protected page.
// Our role parameter should be appear in the page.
String body = getResponseBody();
assertContains(body,
"<title>Protected Page for Examples</title>");
assertContains(body,
"<input type=\"text\" name=\"role\" value=\"bar\"");
return true;
}
}
/*
* Scan the server response body and extract the given
* url, including any path elements.
*/
protected String extractBodyUri(String paramTag, String resource) {
extractUriElements();
List<String> elements = getResponseBodyUriElements();
String fullPath = null;
for (String element : elements) {
int ix = element.indexOf(paramTag);
if (ix > -1) {
ix += paramTag.length();
char delim = element.charAt(ix);
int iy = element.indexOf(resource, ix);
if (iy > -1) {
int lastCharIx = element.indexOf(delim, iy);
fullPath = element.substring(iy, lastCharIx);
// remove any trailing parameters
int paramDelim = fullPath.indexOf(PARAM_DELIM);
if (paramDelim > -1) {
fullPath = fullPath.substring(0, paramDelim);
}
break;
}
}
}
return fullPath;
}
/*
* extract the session id path element (if it exists in the given url)
*/
protected String extractPathSessionId(String url) {
String sessionId = null;
int iStart = url.indexOf(SESSION_PARAMETER_START);
if (iStart > -1) {
iStart += SESSION_PARAMETER_START.length();
String remainder = url.substring(iStart);
StringTokenizer parser = new StringTokenizer(remainder,
SESSION_PATH_PARAMETER_TAILS);
if (parser.hasMoreElements()) {
sessionId = parser.nextToken();
}
else {
sessionId = url.substring(iStart);
}
}
return sessionId;
}
private void assertContains(String body, String expected) {
if (!body.contains(expected)) {
Assert.fail("Response number " + requestCount
+ ": body check failure.\n"
+ "Expected to contain substring: [" + expected
+ "]\nActual: [" + body + "]");
}
}
}
private class FormAuthClient extends FormAuthClientBase {
private FormAuthClient(boolean clientShouldUseCookies,
boolean clientShouldUseHttp11,
boolean serverShouldUseCookies,
boolean serverShouldChangeSessid) throws Exception {
this.clientShouldUseHttp11 = clientShouldUseHttp11;
Tomcat tomcat = getTomcatInstance();
File appDir = new File(System.getProperty("tomcat.test.basedir"), "webapps/examples");
Context ctx = tomcat.addWebapp(null, "/examples",
appDir.getAbsolutePath());
setUseCookies(clientShouldUseCookies);
ctx.setCookies(serverShouldUseCookies);
ctx.addApplicationListener(WsContextListener.class.getName());
TesterMapRealm realm = new TesterMapRealm();
realm.addUser("tomcat", "tomcat");
realm.addUserRole("tomcat", "tomcat");
ctx.setRealm(realm);
tomcat.start();
// Valve pipeline is only established after tomcat starts
Valve[] valves = ctx.getPipeline().getValves();
for (Valve valve : valves) {
if (valve instanceof AuthenticatorBase) {
((AuthenticatorBase)valve)
.setChangeSessionIdOnAuthentication(
serverShouldChangeSessid);
break;
}
}
// Port only known after Tomcat starts
setPort(getPort());
}
}
/**
* Encapsulate the logic needed to run a suitably-configured Tomcat
* instance, send it an HTTP request and process the server response when
* the protected resource is only protected for some HTTP methods. The use
* case of particular interest is when GET and POST are not protected since
* those are the methods used by the login form and the redirect and if
* those methods are not protected the authenticator may not process the
* associated requests.
*/
private class FormAuthClientSelectedMethods extends FormAuthClientBase {
private FormAuthClientSelectedMethods(boolean clientShouldUseCookies,
boolean clientShouldUseHttp11,
boolean serverShouldUseCookies,
boolean serverShouldChangeSessid) throws Exception {
this.clientShouldUseHttp11 = clientShouldUseHttp11;
Tomcat tomcat = getTomcatInstance();
Context ctx = tomcat.addContext(
"", System.getProperty("java.io.tmpdir"));
Tomcat.addServlet(ctx, "SelectedMethods",
new SelectedMethodsServlet());
ctx.addServletMappingDecoded("/test", "SelectedMethods");
// Login servlet just needs to respond "OK". Client will handle
// creating a valid response. No need for a form.
Tomcat.addServlet(ctx, "Login",
new TesterServlet());
ctx.addServletMappingDecoded("/login", "Login");
// Configure the security constraints
SecurityConstraint constraint = new SecurityConstraint();
SecurityCollection collection = new SecurityCollection();
collection.setName("Protect PUT");
collection.addMethod("PUT");
collection.addPatternDecoded("/test");
constraint.addCollection(collection);
constraint.addAuthRole("tomcat");
ctx.addConstraint(constraint);
// Configure authentication
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("FORM");
lc.setLoginPage("/login");
ctx.setLoginConfig(lc);
ctx.getPipeline().addValve(new FormAuthenticator());
setUseCookies(clientShouldUseCookies);
ctx.setCookies(serverShouldUseCookies);
TesterMapRealm realm = new TesterMapRealm();
realm.addUser("tomcat", "tomcat");
realm.addUserRole("tomcat", "tomcat");
ctx.setRealm(realm);
tomcat.start();
// Valve pipeline is only established after tomcat starts
Valve[] valves = ctx.getPipeline().getValves();
for (Valve valve : valves) {
if (valve instanceof AuthenticatorBase) {
((AuthenticatorBase)valve)
.setChangeSessionIdOnAuthentication(
serverShouldChangeSessid);
break;
}
}
// Port only known after Tomcat starts
setPort(getPort());
}
@Override
public boolean isResponseBodyOK() {
if (isResponse303()) {
return true;
}
Assert.assertTrue(getResponseBody(), getResponseBody().contains("OK"));
Assert.assertFalse(getResponseBody().contains("FAIL"));
return true;
}
}
private static final class SelectedMethodsServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final String PARAM = "TestParam";
public static final String VALUE = "TestValue";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
if (VALUE.equals(req.getParameter(PARAM)) &&
req.isUserInRole("tomcat")) {
resp.getWriter().print("OK");
} else {
resp.getWriter().print("FAIL");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Same as GET for this test case
doGet(req, resp);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Same as GET for this test case
doGet(req, resp);
}
}
}

View File

@@ -0,0 +1,89 @@
/**
* 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.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl;
import org.apache.catalina.connector.Request;
public class TestJaspicCallbackHandlerInAuthenticator {
@Test
public void testCustomCallbackHandlerCreation() throws Exception {
testCallbackHandlerCreation("org.apache.catalina.authenticator.TestCallbackHandlerImpl",
TestCallbackHandlerImpl.class);
}
@Test
public void testDefaultCallbackHandlerCreation() throws Exception {
testCallbackHandlerCreation(null, CallbackHandlerImpl.class);
}
private void testCallbackHandlerCreation(String callbackHandlerImplClassName,
Class<?> callbackHandlerImplClass)
throws NoSuchMethodException, SecurityException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
TestAuthenticator authenticator = new TestAuthenticator();
authenticator.setJaspicCallbackHandlerClass(callbackHandlerImplClassName);
Method createCallbackHandlerMethod =
AuthenticatorBase.class.getDeclaredMethod("createCallbackHandler");
createCallbackHandlerMethod.setAccessible(true);
CallbackHandler callbackHandler =
(CallbackHandler) createCallbackHandlerMethod.invoke(authenticator);
Assert.assertTrue(callbackHandlerImplClass.isInstance(callbackHandler));
}
private static class TestAuthenticator extends AuthenticatorBase {
@Override
protected boolean doAuthenticate(Request request, HttpServletResponse response)
throws IOException {
return false;
}
@Override
protected String getAuthMethod() {
return null;
}
}
}
class TestCallbackHandlerImpl implements CallbackHandler {
public TestCallbackHandlerImpl() {
// Default constructor required by reflection
}
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
// don't have to do anything; needed only for instantiation
}
}

View File

@@ -0,0 +1,609 @@
/*
* 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.session.ManagerBase;
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.codec.binary.Base64;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
/**
* Test BasicAuthenticator and NonLoginAuthenticator when a
* SingleSignOn Valve is not active.
*
* <p>
* In the absence of SSO support, these two authenticator classes
* both have quite simple behaviour. By testing them together, we
* can make sure they operate independently and confirm that no
* SSO logic has been accidentally triggered.
*
* <p>
* r1495169 refactored BasicAuthenticator by creating an inner class
* called BasicCredentials. All edge cases associated with strangely
* encoded Base64 credentials are tested thoroughly by TestBasicAuthParser.
* Therefore, TestNonLoginAndBasicAuthenticator only needs to examine
* a sufficient set of test cases to verify the interface between
* BasicAuthenticator and BasicCredentials, which it does by running
* each test under a separate tomcat instance.
*/
public class TestNonLoginAndBasicAuthenticator extends TomcatBaseTest {
protected static final boolean USE_COOKIES = true;
protected static final boolean NO_COOKIES = !USE_COOKIES;
private static final String USER = "user";
private static final String PWD = "pwd";
private static final String ROLE = "role";
private static final String NICE_METHOD = "Basic";
private static final String HTTP_PREFIX = "http://localhost:";
private static final String CONTEXT_PATH_NOLOGIN = "/nologin";
private static final String CONTEXT_PATH_LOGIN = "/login";
private static final String URI_PROTECTED = "/protected";
private static final String URI_PUBLIC = "/anyoneCanAccess";
private static final int SHORT_SESSION_TIMEOUT_SECS = 1;
private static final int MANAGER_SCAN_INTERVAL_SECS = 2;
private static final int MANAGER_EXPIRE_SESSIONS_FAST = 1;
private static final int EXTRA_DELAY_SECS = 5;
private static final long TIMEOUT_DELAY_MSECS =
((SHORT_SESSION_TIMEOUT_SECS +
(MANAGER_SCAN_INTERVAL_SECS * MANAGER_EXPIRE_SESSIONS_FAST) +
EXTRA_DELAY_SECS) * 1000);
private static final String CLIENT_AUTH_HEADER = "authorization";
private static final String SERVER_AUTH_HEADER = "WWW-Authenticate";
private static final String SERVER_COOKIE_HEADER = "Set-Cookie";
private static final String CLIENT_COOKIE_HEADER = "Cookie";
private static final BasicCredentials NO_CREDENTIALS = null;
private static final BasicCredentials GOOD_CREDENTIALS =
new BasicCredentials(NICE_METHOD, USER, PWD);
private static final BasicCredentials STRANGE_CREDENTIALS =
new BasicCredentials("bAsIc", USER, PWD);
private static final BasicCredentials BAD_CREDENTIALS =
new BasicCredentials(NICE_METHOD, USER, "wrong");
private static final BasicCredentials BAD_METHOD =
new BasicCredentials("BadMethod", USER, PWD);
private Tomcat tomcat;
private Context basicContext;
private Context nonloginContext;
private List<String> cookies;
/*
* Try to access an unprotected resource in a webapp that
* does not have a login method defined.
* This should be permitted.
*/
@Test
public void testAcceptPublicNonLogin() throws Exception {
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, NO_COOKIES,
HttpServletResponse.SC_OK);
}
/*
* Try to access a protected resource in a webapp that
* does not have a login method defined.
* This should be rejected with SC_FORBIDDEN 403 status.
*/
@Test
public void testRejectProtectedNonLogin() throws Exception {
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, NO_COOKIES,
HttpServletResponse.SC_FORBIDDEN);
}
/*
* Try to access an unprotected resource in a webapp that
* has a BASIC login method defined.
* This should be permitted without a challenge.
*/
@Test
public void testAcceptPublicBasic() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PUBLIC, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
}
/*
* Try to access a protected resource in a webapp that
* has a BASIC login method defined. The access will be
* challenged with 401 SC_UNAUTHORIZED, and then be permitted
* once authenticated.
*/
@Test
public void testAcceptProtectedBasic() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
}
/*
* This is the same as testAcceptProtectedBasic (above), except
* using an invalid password.
*/
@Test
public void testAuthMethodBadCredentials() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, BAD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
}
/*
* This is the same as testAcceptProtectedBasic (above), except
* to verify the server follows RFC2617 by treating the auth-scheme
* token as case-insensitive.
*/
@Test
public void testAuthMethodCaseBasic() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, STRANGE_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
}
/*
* This is the same as testAcceptProtectedBasic (above), except
* using an invalid authentication method.
*
* Note: the container ensures the Basic login method is called.
* BasicAuthenticator does not find the expected authentication
* header method, and so does not extract any credentials.
*
* The request is rejected with 401 SC_UNAUTHORIZED status. RFC2616
* says the response body should identify the auth-schemes that are
* acceptable for the container.
*/
@Test
public void testAuthMethodBadMethod() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, BAD_METHOD,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
}
/*
* The default behaviour of BASIC authentication does NOT create
* a session on the server. Verify that the client is required to
* send a valid authenticate header with every request to access
* protected resources.
*/
@Test
public void testBasicLoginWithoutSession() throws Exception {
// this section is identical to testAuthMethodCaseBasic
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
// next, try to access the protected resource while not providing
// credentials. This confirms the server has not retained any state
// data which might allow it to authenticate the client. Expect
// to be challenged with 401 SC_UNAUTHORIZED.
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
// finally, provide credentials to confirm the resource
// can still be accessed with an authentication header.
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
}
/*
* Test the optional behaviour of BASIC authentication to create
* a session on the server. The server will return a session cookie.
*
* 1. try to access a protected resource without credentials, so
* get Unauthorized status.
* 2. try to access a protected resource when providing credentials,
* so get OK status and a server session cookie.
* 3. access the protected resource once more using a session cookie.
* 4. repeat using the session cookie.
*
* Note: The FormAuthenticator is a two-step process and is protected
* from session fixation attacks by the default AuthenticatorBase
* changeSessionIdOnAuthentication setting of true. However,
* BasicAuthenticator is a one-step process and so the
* AuthenticatorBase does not reissue the sessionId.
*/
@Test
public void testBasicLoginSessionPersistence() throws Exception {
setAlwaysUseSession();
// this section is identical to testAuthMethodCaseBasic
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
// confirm the session is not recognised by the server alone
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
// now provide the harvested session cookie for authentication
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
USE_COOKIES, HttpServletResponse.SC_OK);
// finally, do it again with the cookie to be sure
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
USE_COOKIES, HttpServletResponse.SC_OK);
}
/*
* Verify the timeout mechanism works for BASIC sessions. This test
* follows the flow of testBasicLoginSessionPersistence (above).
*/
@Test
public void testBasicLoginSessionTimeout() throws Exception {
setAlwaysUseSession();
setRapidSessionTimeout();
// this section is identical to testAuthMethodCaseBasic
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
// now provide the harvested session cookie for authentication
List<String> originalCookies = cookies;
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
USE_COOKIES, HttpServletResponse.SC_OK);
// Force session to expire one second from now
forceSessionMaxInactiveInterval(
(Context) getTomcatInstance().getHost().findChild(CONTEXT_PATH_LOGIN),
SHORT_SESSION_TIMEOUT_SECS);
// allow the session to time out and lose authentication
Thread.sleep(TIMEOUT_DELAY_MSECS);
// provide the harvested session cookie for authentication
// to confirm it has expired
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
USE_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
// finally, do BASIC reauthentication and get another session
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
// slightly paranoid verification
boolean sameCookies = originalCookies.equals(cookies);
Assert.assertTrue(!sameCookies);
}
/*
* Logon to access a protected resource in a webapp that uses
* BASIC authentication. Then try to access a protected resource
* in a different webapp which does not have a login method.
* This should be rejected with SC_FORBIDDEN 403 status, confirming
* there has been no cross-authentication between the webapps.
*/
@Test
public void testBasicLoginRejectProtected() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
NO_COOKIES, HttpServletResponse.SC_FORBIDDEN);
}
/*
* Try to use the session cookie from the BASIC webapp to request
* access to the webapp that does not have a login method. (This
* is equivalent to Single Signon, but without the Valve.)
*
* Verify there is no cross-authentication when using similar logic
* to testBasicLoginRejectProtected (above).
*
* This should be rejected with SC_FORBIDDEN 403 status.
*/
@Test
public void testBasicLoginRejectProtectedWithSession() throws Exception {
setAlwaysUseSession();
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS,
NO_COOKIES, HttpServletResponse.SC_OK);
// use the session cookie harvested with the other webapp
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
USE_COOKIES, HttpServletResponse.SC_FORBIDDEN);
}
private void doTestNonLogin(String uri, boolean useCookie,
int expectedRC) throws Exception {
Map<String,List<String>> reqHeaders = new HashMap<>();
Map<String,List<String>> respHeaders = new HashMap<>();
if (useCookie) {
addCookies(reqHeaders);
}
ByteChunk bc = new ByteChunk();
int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders,
respHeaders);
if (expectedRC != HttpServletResponse.SC_OK) {
Assert.assertEquals(expectedRC, rc);
Assert.assertTrue(bc.getLength() > 0);
}
else {
Assert.assertEquals("OK", bc.toString());
}
}
private void doTestBasic(String uri, BasicCredentials credentials,
boolean useCookie, int expectedRC) throws Exception {
Map<String,List<String>> reqHeaders = new HashMap<>();
Map<String,List<String>> respHeaders = new HashMap<>();
if (useCookie) {
addCookies(reqHeaders);
}
else {
if (credentials != null) {
List<String> auth = new ArrayList<>();
auth.add(credentials.getCredentials());
reqHeaders.put(CLIENT_AUTH_HEADER, auth);
}
}
ByteChunk bc = new ByteChunk();
int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders,
respHeaders);
if (expectedRC != HttpServletResponse.SC_OK) {
Assert.assertEquals(expectedRC, rc);
Assert.assertTrue(bc.getLength() > 0);
if (expectedRC == HttpServletResponse.SC_UNAUTHORIZED) {
// The server should identify the acceptable method(s)
boolean methodFound = false;
List<String> authHeaders = respHeaders.get(SERVER_AUTH_HEADER);
for (String authHeader : authHeaders) {
if (authHeader.indexOf(NICE_METHOD) > -1) {
methodFound = true;
break;
}
}
Assert.assertTrue(methodFound);
}
}
else {
Assert.assertEquals("OK", bc.toString());
List<String> newCookies = respHeaders.get(SERVER_COOKIE_HEADER);
if (newCookies != null) {
// harvest cookies whenever the server sends some new ones
saveCookies(respHeaders);
}
}
}
/*
* setup two webapps for every test
*
* note: the super class tearDown method will stop tomcat
*/
@Override
public void setUp() throws Exception {
super.setUp();
// create a tomcat server using the default in-memory Realm
tomcat = getTomcatInstance();
// add the test user and role to the Realm
tomcat.addUser(USER, PWD);
tomcat.addRole(USER, ROLE);
// setup both NonLogin and Login webapps
setUpNonLogin();
setUpLogin();
tomcat.start();
}
private void setUpNonLogin() throws Exception {
// Must have a real docBase for webapps - just use temp
nonloginContext = tomcat.addContext(CONTEXT_PATH_NOLOGIN,
System.getProperty("java.io.tmpdir"));
// Add protected servlet to the context
Tomcat.addServlet(nonloginContext, "TesterServlet1", new TesterServlet());
nonloginContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1");
SecurityCollection collection1 = new SecurityCollection();
collection1.addPatternDecoded(URI_PROTECTED);
SecurityConstraint sc1 = new SecurityConstraint();
sc1.addAuthRole(ROLE);
sc1.addCollection(collection1);
nonloginContext.addConstraint(sc1);
// Add unprotected servlet to the context
Tomcat.addServlet(nonloginContext, "TesterServlet2", new TesterServlet());
nonloginContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2");
SecurityCollection collection2 = new SecurityCollection();
collection2.addPatternDecoded(URI_PUBLIC);
SecurityConstraint sc2 = new SecurityConstraint();
// do not add a role - which signals access permitted without one
sc2.addCollection(collection2);
nonloginContext.addConstraint(sc2);
// Configure the authenticator and inherit the Realm from Engine
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("NONE");
nonloginContext.setLoginConfig(lc);
AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator();
nonloginContext.getPipeline().addValve(nonloginAuthenticator);
}
private void setUpLogin() throws Exception {
// Must have a real docBase for webapps - just use temp
basicContext = tomcat.addContext(CONTEXT_PATH_LOGIN,
System.getProperty("java.io.tmpdir"));
// Add protected servlet to the context
Tomcat.addServlet(basicContext, "TesterServlet3", new TesterServlet());
basicContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3");
SecurityCollection collection = new SecurityCollection();
collection.addPatternDecoded(URI_PROTECTED);
SecurityConstraint sc = new SecurityConstraint();
sc.addAuthRole(ROLE);
sc.addCollection(collection);
basicContext.addConstraint(sc);
// Add unprotected servlet to the context
Tomcat.addServlet(basicContext, "TesterServlet4", new TesterServlet());
basicContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet4");
SecurityCollection collection2 = new SecurityCollection();
collection2.addPatternDecoded(URI_PUBLIC);
SecurityConstraint sc2 = new SecurityConstraint();
// do not add a role - which signals access permitted without one
sc2.addCollection(collection2);
basicContext.addConstraint(sc2);
// Configure the authenticator and inherit the Realm from Engine
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("BASIC");
basicContext.setLoginConfig(lc);
AuthenticatorBase basicAuthenticator = new BasicAuthenticator();
basicContext.getPipeline().addValve(basicAuthenticator);
}
/*
* Force non-default behaviour for both Authenticators
*/
private void setAlwaysUseSession() {
((AuthenticatorBase)basicContext.getAuthenticator())
.setAlwaysUseSession(true);
((AuthenticatorBase)nonloginContext.getAuthenticator())
.setAlwaysUseSession(true);
}
/*
* Force rapid timeout scanning for the Basic Authentication webapp
* The StandardManager default service cycle time is 10 seconds,
* with a session expiry scan every 6 cycles.
*/
private void setRapidSessionTimeout() {
basicContext.getParent().getParent().setBackgroundProcessorDelay(
MANAGER_SCAN_INTERVAL_SECS);
((ManagerBase) basicContext.getManager())
.setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST);
}
/*
* Encapsulate the logic to generate an HTTP header
* for BASIC Authentication.
* Note: only used internally, so no need to validate arguments.
*/
private static final class BasicCredentials {
private final String method;
private final String username;
private final String password;
private final String credentials;
private BasicCredentials(String aMethod,
String aUsername, String aPassword) {
method = aMethod;
username = aUsername;
password = aPassword;
String userCredentials = username + ":" + password;
byte[] credentialsBytes =
userCredentials.getBytes(StandardCharsets.ISO_8859_1);
String base64auth = Base64.encodeBase64String(credentialsBytes);
credentials= method + " " + base64auth;
}
private String getCredentials() {
return credentials;
}
}
/*
* extract and save the server cookies from the incoming response
*/
protected void saveCookies(Map<String,List<String>> respHeaders) {
// we only save the Cookie values, not header prefix
List<String> cookieHeaders = respHeaders.get(SERVER_COOKIE_HEADER);
if (cookieHeaders == null) {
cookies = null;
} else {
cookies = new ArrayList<>(cookieHeaders.size());
for (String cookieHeader : cookieHeaders) {
cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';')));
}
}
}
/*
* add all saved cookies to the outgoing request
*/
protected void addCookies(Map<String,List<String>> reqHeaders) {
if ((cookies != null) && (cookies.size() > 0)) {
StringBuilder cookieHeader = new StringBuilder();
boolean first = true;
for (String cookie : cookies) {
if (!first) {
cookieHeader.append(';');
} else {
first = false;
}
cookieHeader.append(cookie);
}
List<String> cookieHeaderList = new ArrayList<>(1);
cookieHeaderList.add(cookieHeader.toString());
reqHeaders.put(CLIENT_COOKIE_HEADER, cookieHeaderList);
}
}
}

View File

@@ -0,0 +1,683 @@
/*
* 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Session;
import org.apache.catalina.session.ManagerBase;
import org.apache.catalina.startup.TesterServletEncodeUrl;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.codec.binary.Base64;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
/**
* Test BasicAuthenticator and NonLoginAuthenticator when a
* SingleSignOn Valve is active.
*
* <p>
* In the absence of SSO support, a webapp using NonLoginAuthenticator
* simply cannot access protected resources. These tests exercise the
* the way successfully authenticating a different webapp under the
* BasicAuthenticator triggers the additional SSO logic for both webapps.
*
* <p>
* The two Authenticators are thoroughly exercised by two other unit test
* classes: TestBasicAuthParser and TestNonLoginAndBasicAuthenticator.
* This class mainly examines the way the Single SignOn Valve interacts with
* two webapps when the second cannot be authenticated directly, but needs
* to inherit its authentication via the other.
*
* <p>
* When the server and client can both use cookies, the authentication
* is preserved through the exchange of a JSSOSESSIONID cookie, which
* is different to the individual and unique JSESSIONID cookies assigned
* separately to the two webapp sessions.
*
* <p>
* The other situation examined is where the server returns authentication
* cookies, but the client is configured to ignore them. The Tomcat
* documentation clearly states that SSO <i>requires</i> the client to
* support cookies, so access to resources in other webapp containers
* receives no SSO assistance.
*/
public class TestSSOnonLoginAndBasicAuthenticator extends TomcatBaseTest {
protected static final boolean USE_COOKIES = true;
protected static final boolean NO_COOKIES = !USE_COOKIES;
private static final String USER = "user";
private static final String PWD = "pwd";
private static final String ROLE = "role";
private static final String NICE_METHOD = "Basic";
private static final String HTTP_PREFIX = "http://localhost:";
private static final String CONTEXT_PATH_NOLOGIN = "/nologin";
private static final String CONTEXT_PATH_LOGIN = "/login";
private static final String URI_PROTECTED = "/protected";
private static final String URI_PUBLIC = "/anyoneCanAccess";
// session expiry in web.xml is defined in minutes
private static final int SHORT_SESSION_TIMEOUT_MINS = 1;
private static final int LONG_SESSION_TIMEOUT_MINS = 2;
// we don't change the expiry scan interval - just the iteration count
private static final int MANAGER_SCAN_INTERVAL_SECS = 10;
private static final int MANAGER_EXPIRE_SESSIONS_FAST = 1;
// now compute some delays - beware of the units!
private static final int EXTRA_DELAY_SECS = 5;
private static final int TIMEOUT_WAIT_SECS = EXTRA_DELAY_SECS +
(MANAGER_SCAN_INTERVAL_SECS * MANAGER_EXPIRE_SESSIONS_FAST) * 5;
private static final String CLIENT_AUTH_HEADER = "authorization";
private static final String SERVER_AUTH_HEADER = "WWW-Authenticate";
private static final String SERVER_COOKIE_HEADER = "Set-Cookie";
private static final String CLIENT_COOKIE_HEADER = "Cookie";
private static final String ENCODE_SESSION_PARAM = "jsessionid";
private static final String ENCODE_SSOSESSION_PARAM = "jssosessionid";
private static final
TestSSOnonLoginAndBasicAuthenticator.BasicCredentials
NO_CREDENTIALS = null;
private static final
TestSSOnonLoginAndBasicAuthenticator.BasicCredentials
GOOD_CREDENTIALS =
new TestSSOnonLoginAndBasicAuthenticator.BasicCredentials(
NICE_METHOD, USER, PWD);
private Tomcat tomcat;
private Context basicContext;
private Context nonloginContext;
private List<String> cookies;
private String encodedURL;
/*
* Run some sanity checks without an established SSO session
* to make sure the test environment is correct.
*/
@Test
public void testEssentialEnvironment() throws Exception {
// should be permitted to access an unprotected resource.
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC,
USE_COOKIES, HttpServletResponse.SC_OK);
// should not be permitted to access a protected resource
// with the two Authenticators used in the remaining tests.
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
USE_COOKIES, HttpServletResponse.SC_FORBIDDEN);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void testEssentialEnvironmentWithoutCookies() throws Exception {
// should be permitted to access an unprotected resource.
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC,
NO_COOKIES, HttpServletResponse.SC_OK);
// should not be permitted to access a protected resource
// with the two Authenticators used in the remaining tests.
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
NO_COOKIES, HttpServletResponse.SC_FORBIDDEN);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, NO_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
}
/*
* Logon to access a protected resource using BASIC authentication,
* which will establish an SSO session.
* Wait until the SSO session times-out, then try to re-access
* the resource. This should be rejected with SC_FORBIDDEN 401 status.
*
* Note: this test should run for ~10 seconds.
*/
@Test
public void testBasicAccessAndSessionTimeout() throws Exception {
setRapidSessionTimeoutDetection();
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
GOOD_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_OK);
// verify the SSOID exists as a cookie
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
GOOD_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_OK);
// make the session time out and lose authentication
doImminentSessionTimeout(basicContext);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
}
/*
* Logon to access a protected resource using BASIC authentication,
* which will establish an SSO session.
* Immediately try to access a protected resource in the NonLogin
* webapp while providing the SSO session cookie received from the
* first webapp. This should be successful with SC_OK 200 status.
*/
@Test
public void testBasicLoginThenAcceptWithCookies() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, NO_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK);
// send the cookie which proves we have an authenticated SSO session
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
USE_COOKIES, HttpServletResponse.SC_OK);
}
/*
* Logon to access a protected resource using BASIC authentication,
* which will establish an SSO session.
* Immediately try to access a protected resource in the NonLogin
* webapp, but without sending the SSO session cookie.
* This should be rejected with SC_FORBIDDEN 403 status.
*/
@Test
public void testBasicLoginThenRejectWithoutCookie() throws Exception {
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
GOOD_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_OK);
// fail to send the authentication cookie to the other webapp.
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
NO_COOKIES, HttpServletResponse.SC_FORBIDDEN);
}
/*
* Logon to access a protected resource using BASIC authentication,
* which will establish an SSO session.
* Then try to access a protected resource in the NonLogin
* webapp by sending the JSESSIONID from the redirect header.
* The access request should be rejected because the Basic webapp's
* sessionID is not valid for any other container.
*/
@Test
public void testBasicAccessThenAcceptAuthWithUri() throws Exception {
setAlwaysUseSession();
// first, fail to access the protected resource without credentials
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, NO_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
// now, access the protected resource with good credentials
// to establish the session
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
GOOD_CREDENTIALS, NO_COOKIES,
HttpServletResponse.SC_OK);
// next, access it again to harvest the session id url parameter
String forwardParam = "?nextUrl=" + CONTEXT_PATH_LOGIN + URI_PROTECTED;
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED + forwardParam,
GOOD_CREDENTIALS, NO_COOKIES,
HttpServletResponse.SC_OK);
// verify the sessionID was encoded in the absolute URL
String firstEncodedURL = encodedURL;
Assert.assertTrue(firstEncodedURL.contains(ENCODE_SESSION_PARAM));
// access the protected resource with the encoded url (with session id)
doTestBasic(firstEncodedURL + forwardParam,
NO_CREDENTIALS, NO_COOKIES,
HttpServletResponse.SC_OK);
// verify the sessionID has not changed
// verify the SSO sessionID was not encoded
String secondEncodedURL = encodedURL;
Assert.assertEquals(firstEncodedURL, secondEncodedURL);
Assert.assertFalse(firstEncodedURL.contains(ENCODE_SSOSESSION_PARAM));
// extract the first container's session ID
int ix = secondEncodedURL.indexOf(ENCODE_SESSION_PARAM);
String sessionId = secondEncodedURL.substring(ix);
// expect to fail using that sessionID in a different container
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED + ";" + sessionId,
NO_COOKIES, HttpServletResponse.SC_FORBIDDEN);
}
/*
* Logon to access a protected resource using BASIC authentication,
* which will establish an SSO session.
* Immediately try to access a protected resource in the NonLogin
* webapp while providing the SSO session cookie received from the
* first webapp. This should be successful with SC_OK 200 status.
*
* Then, wait long enough for the BASIC session to expire. (The SSO
* session should remain active because the NonLogin session has
* not yet expired).
* Try to access the protected resource again, before the SSO session
* has expired. This should be successful with SC_OK 200 status.
*
* Finally, wait for the non-login session to expire and try again..
* This should be rejected with SC_FORBIDDEN 403 status.
*
* (see bugfix https://bz.apache.org/bugzilla/show_bug.cgi?id=52303)
*
* Note: this test should run for ~20 seconds.
*/
@Test
public void testBasicExpiredAcceptProtectedWithCookies() throws Exception {
setRapidSessionTimeoutDetection();
// begin with a repeat of testBasicLoginAcceptProtectedWithCookies
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
GOOD_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_OK);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
USE_COOKIES, HttpServletResponse.SC_OK);
// wait long enough for the BASIC session to expire,
// but not long enough for the NonLogin session expiry.
doImminentSessionTimeout(basicContext);
// this successful NonLogin access should replenish the
// the individual session expiry time and keep the SSO session alive
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
USE_COOKIES, HttpServletResponse.SC_OK);
// wait long enough for the NonLogin session to expire,
// which will also tear down the SSO session at the same time.
doImminentSessionTimeout(nonloginContext);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES,
HttpServletResponse.SC_FORBIDDEN);
doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED,
NO_CREDENTIALS, USE_COOKIES,
HttpServletResponse.SC_UNAUTHORIZED);
}
public void doTestNonLogin(String uri, boolean useCookie,
int expectedRC) throws Exception {
Map<String,List<String>> reqHeaders = new HashMap<>();
Map<String,List<String>> respHeaders = new HashMap<>();
if (useCookie && (cookies != null)) {
addCookies(reqHeaders);
}
ByteChunk bc = new ByteChunk();
int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders,
respHeaders);
if (expectedRC != HttpServletResponse.SC_OK) {
Assert.assertEquals(expectedRC, rc);
Assert.assertTrue(bc.getLength() > 0);
}
else {
Assert.assertEquals("OK", bc.toString());
}
}
private void doTestBasic(String uri,
TestSSOnonLoginAndBasicAuthenticator.BasicCredentials credentials,
boolean useCookie, int expectedRC) throws Exception {
Map<String,List<String>> reqHeaders = new HashMap<>();
Map<String,List<String>> respHeaders = new HashMap<>();
if (useCookie && (cookies != null)) {
addCookies(reqHeaders);
}
else {
if (credentials != null) {
List<String> auth = new ArrayList<>();
auth.add(credentials.getCredentials());
reqHeaders.put(CLIENT_AUTH_HEADER, auth);
}
}
ByteChunk bc = new ByteChunk();
int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders,
respHeaders);
Assert.assertEquals("Unexpected Return Code", expectedRC, rc);
if (expectedRC != HttpServletResponse.SC_OK) {
Assert.assertTrue(bc.getLength() > 0);
if (expectedRC == HttpServletResponse.SC_UNAUTHORIZED) {
// The server should identify the acceptable method(s)
boolean methodFound = false;
List<String> authHeaders = respHeaders.get(SERVER_AUTH_HEADER);
for (String authHeader : authHeaders) {
if (authHeader.indexOf(NICE_METHOD) > -1) {
methodFound = true;
break;
}
}
Assert.assertTrue(methodFound);
}
}
else {
String thePage = bc.toString();
Assert.assertNotNull(thePage);
Assert.assertTrue(thePage.startsWith("OK"));
if (useCookie) {
List<String> newCookies = respHeaders.get(SERVER_COOKIE_HEADER);
if (newCookies != null) {
// harvest cookies whenever the server sends some new ones
cookies = newCookies;
}
}
else {
encodedURL = "";
final String start = "<a href=\"";
final String end = "\">";
int iStart = thePage.indexOf(start);
int iEnd = 0;
if (iStart > -1) {
iStart += start.length();
iEnd = thePage.indexOf(end, iStart);
if (iEnd > -1) {
encodedURL = thePage.substring(iStart, iEnd);
}
}
}
}
}
/*
* setup two webapps for every test
*
* note: the super class tearDown method will stop tomcat
*/
@Override
public void setUp() throws Exception {
super.setUp();
// create a tomcat server using the default in-memory Realm
tomcat = getTomcatInstance();
// associate the SingeSignOn Valve before the Contexts
SingleSignOn sso = new SingleSignOn();
tomcat.getHost().getPipeline().addValve(sso);
// add the test user and role to the Realm
tomcat.addUser(USER, PWD);
tomcat.addRole(USER, ROLE);
// setup both NonLogin and Login webapps
setUpNonLogin();
setUpLogin();
tomcat.start();
}
@Override
public void tearDown() throws Exception {
tomcat.stop();
}
private void setUpNonLogin() throws Exception {
// Must have a real docBase for webapps - just use temp
nonloginContext = tomcat.addContext(CONTEXT_PATH_NOLOGIN,
System.getProperty("java.io.tmpdir"));
nonloginContext.setSessionTimeout(LONG_SESSION_TIMEOUT_MINS);
// Add protected servlet to the context
Tomcat.addServlet(nonloginContext, "TesterServlet1",
new TesterServletEncodeUrl());
nonloginContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1");
SecurityCollection collection1 = new SecurityCollection();
collection1.addPatternDecoded(URI_PROTECTED);
SecurityConstraint sc1 = new SecurityConstraint();
sc1.addAuthRole(ROLE);
sc1.addCollection(collection1);
nonloginContext.addConstraint(sc1);
// Add unprotected servlet to the context
Tomcat.addServlet(nonloginContext, "TesterServlet2",
new TesterServletEncodeUrl());
nonloginContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2");
SecurityCollection collection2 = new SecurityCollection();
collection2.addPatternDecoded(URI_PUBLIC);
SecurityConstraint sc2 = new SecurityConstraint();
// do not add a role - which signals access permitted without one
sc2.addCollection(collection2);
nonloginContext.addConstraint(sc2);
// Configure the authenticator and inherit the Realm from Engine
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("NONE");
nonloginContext.setLoginConfig(lc);
AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator();
nonloginContext.getPipeline().addValve(nonloginAuthenticator);
}
private void setUpLogin() throws Exception {
// Must have a real docBase for webapps - just use temp
basicContext = tomcat.addContext(CONTEXT_PATH_LOGIN,
System.getProperty("java.io.tmpdir"));
basicContext.setSessionTimeout(SHORT_SESSION_TIMEOUT_MINS);
// Add protected servlet to the context
Tomcat.addServlet(basicContext, "TesterServlet3",
new TesterServletEncodeUrl());
basicContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3");
SecurityCollection collection = new SecurityCollection();
collection.addPatternDecoded(URI_PROTECTED);
SecurityConstraint sc = new SecurityConstraint();
sc.addAuthRole(ROLE);
sc.addCollection(collection);
basicContext.addConstraint(sc);
// Add unprotected servlet to the context
Tomcat.addServlet(basicContext, "TesterServlet4",
new TesterServletEncodeUrl());
basicContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet4");
SecurityCollection collection2 = new SecurityCollection();
collection2.addPatternDecoded(URI_PUBLIC);
SecurityConstraint sc2 = new SecurityConstraint();
// do not add a role - which signals access permitted without one
sc2.addCollection(collection2);
basicContext.addConstraint(sc2);
// Configure the authenticator and inherit the Realm from Engine
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("BASIC");
basicContext.setLoginConfig(lc);
AuthenticatorBase basicAuthenticator = new BasicAuthenticator();
basicContext.getPipeline().addValve(basicAuthenticator);
}
/*
* extract and save the server cookies from the incoming response
*/
protected void saveCookies(Map<String,List<String>> respHeaders) {
// we only save the Cookie values, not header prefix
List<String> cookieHeaders = respHeaders.get(SERVER_COOKIE_HEADER);
if (cookieHeaders == null) {
cookies = null;
} else {
cookies = new ArrayList<>(cookieHeaders.size());
for (String cookieHeader : cookieHeaders) {
cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';')));
}
}
}
/*
* add all saved cookies to the outgoing request
*/
protected void addCookies(Map<String,List<String>> reqHeaders) {
if ((cookies != null) && (cookies.size() > 0)) {
StringBuilder cookieHeader = new StringBuilder();
boolean first = true;
for (String cookie : cookies) {
if (!first) {
cookieHeader.append(';');
} else {
first = false;
}
cookieHeader.append(cookie);
}
List<String> cookieHeaderList = new ArrayList<>(1);
cookieHeaderList.add(cookieHeader.toString());
reqHeaders.put(CLIENT_COOKIE_HEADER, cookieHeaderList);
}
}
/*
* Force non-default behaviour for both Authenticators.
* The session id will not be regenerated after authentication,
* which is less secure but needed for browsers that will not
* handle cookies.
*/
private void setAlwaysUseSession() {
((AuthenticatorBase) basicContext.getAuthenticator())
.setAlwaysUseSession(true);
((AuthenticatorBase) nonloginContext.getAuthenticator())
.setAlwaysUseSession(true);
}
/*
* Force faster timeout for an active Container than can
* be defined in web.xml. By getting to the active Session we
* can choose seconds instead of minutes.
* Note: shamelessly cloned from ManagerBase - beware of synch issues
* on the underlying sessions.
*/
private void doImminentSessionTimeout(Context activeContext) {
ManagerBase manager = (ManagerBase) activeContext.getManager();
Session[] sessions = manager.findSessions();
for (int i = 0; i < sessions.length; i++) {
if (sessions[i]!=null && sessions[i].isValid()) {
sessions[i].setMaxInactiveInterval(EXTRA_DELAY_SECS);
// leave it to be expired by the manager
}
}
try {
Thread.sleep(EXTRA_DELAY_SECS * 1000);
} catch (InterruptedException ie) {
// ignored
}
// Paranoid verification that active sessions have now gone
int count = 0;
sessions = manager.findSessions();
while (sessions.length != 0 && count < TIMEOUT_WAIT_SECS) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
}
sessions = manager.findSessions();
count++;
}
sessions = manager.findSessions();
Assert.assertTrue(sessions.length == 0);
}
/*
* Force rapid timeout scanning for both webapps
* The StandardManager default service cycle time is 10 seconds,
* with a session expiry scan every 6 cycles.
*/
private void setRapidSessionTimeoutDetection() {
((ManagerBase) basicContext.getManager())
.setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST);
((ManagerBase) nonloginContext.getManager())
.setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST);
}
/*
* Encapsulate the logic to generate an HTTP header
* for BASIC Authentication.
* Note: only used internally, so no need to validate arguments.
*/
private static final class BasicCredentials {
private final String method;
private final String username;
private final String password;
private final String credentials;
private BasicCredentials(String aMethod,
String aUsername, String aPassword) {
method = aMethod;
username = aUsername;
password = aPassword;
String userCredentials = username + ":" + password;
byte[] credentialsBytes =
userCredentials.getBytes(StandardCharsets.ISO_8859_1);
String base64auth = Base64.encodeBase64String(credentialsBytes);
credentials= method + " " + base64auth;
}
private String getCredentials() {
return credentials;
}
}
}

View File

@@ -0,0 +1,502 @@
/*
* 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.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
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;
/**
* Test DigestAuthenticator and NonLoginAuthenticator when a
* SingleSignOn Valve is active.
*
* <p>
* In the absence of SSO support, a webapp using NonLoginAuthenticator
* simply cannot access protected resources. These tests exercise the
* the way successfully authenticating a different webapp under the
* DigestAuthenticator triggers the additional SSO logic for both webapps.
*
* <p>
* Note: these tests are intended to exercise the SSO logic of the
* Authenticator, but not to comprehensively test all of its logic paths.
* That is the responsibility of the non-SSO test suite.
*/
public class TestSSOnonLoginAndDigestAuthenticator extends TomcatBaseTest {
private static final String USER = "user";
private static final String PWD = "pwd";
private static final String ROLE = "role";
private static final String HTTP_PREFIX = "http://localhost:";
private static final String CONTEXT_PATH_NOLOGIN = "/nologin";
private static final String CONTEXT_PATH_DIGEST = "/digest";
private static final String URI_PROTECTED = "/protected";
private static final String URI_PUBLIC = "/anyoneCanAccess";
private static final int SHORT_TIMEOUT_SECS = 4;
private static final long SHORT_TIMEOUT_DELAY_MSECS =
((SHORT_TIMEOUT_SECS + 3) * 1000);
private static final int LONG_TIMEOUT_SECS = 10;
private static final long LONG_TIMEOUT_DELAY_MSECS =
((LONG_TIMEOUT_SECS + 2) * 1000);
private static final String CLIENT_AUTH_HEADER = "authorization";
private static final String OPAQUE = "opaque";
private static final String NONCE = "nonce";
private static final String REALM = "realm";
private static final String CNONCE = "cnonce";
private static String NC1 = "00000001";
private static String NC2 = "00000002";
private static String QOP = "auth";
private static String SERVER_COOKIES = "Set-Cookie";
private static String BROWSER_COOKIES = "Cookie";
private List<String> cookies;
/*
* Try to access an unprotected resource without an
* established SSO session.
* This should be permitted.
*/
@Test
public void testAcceptPublicNonLogin() throws Exception {
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC,
true, false, 200);
}
/*
* Try to access a protected resource without an established
* SSO session.
* This should be rejected with SC_FORBIDDEN 403 status.
*/
@Test
public void testRejectProtectedNonLogin() throws Exception {
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
false, true, 403);
}
/*
* Logon to access a protected resource using DIGEST authentication,
* which will establish an SSO session.
* Wait until the SSO session times-out, then try to re-access
* the resource.
* This should be rejected with SC_FORBIDDEN 401 status, which
* will then be followed by successful re-authentication.
*/
@Test
public void testDigestLoginSessionTimeout() throws Exception {
doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED,
true, 401, true, true, NC1, CNONCE, QOP, true);
// wait long enough for my session to expire
Thread.sleep(LONG_TIMEOUT_DELAY_MSECS);
// must change the client nonce to succeed
doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED,
true, 401, true, true, NC2, CNONCE, QOP, true);
}
/*
* Logon to access a protected resource using DIGEST authentication,
* which will establish an SSO session.
* Immediately try to access a protected resource in the NonLogin
* webapp, but without sending the SSO session cookie.
* This should be rejected with SC_FORBIDDEN 403 status.
*/
@Test
public void testDigestLoginRejectProtectedWithoutCookies() throws Exception {
doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED,
true, 401, true, true, NC1, CNONCE, QOP, true);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
false, true, 403);
}
/*
* Logon to access a protected resource using DIGEST authentication,
* which will establish an SSO session.
* Immediately try to access a protected resource in the NonLogin
* webapp while sending the SSO session cookie provided by the
* first webapp.
* This should be successful with SC_OK 200 status.
*/
@Test
public void testDigestLoginAcceptProtectedWithCookies() throws Exception {
doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED,
true, 401, true, true, NC1, CNONCE, QOP, true);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
true, false, 200);
}
/*
* Logon to access a protected resource using DIGEST authentication,
* which will establish an SSO session.
* Immediately try to access a protected resource in the NonLogin
* webapp while sending the SSO session cookie provided by the
* first webapp.
* This should be successful with SC_OK 200 status.
*
* Then, wait long enough for the DIGEST session to expire. (The SSO
* session should remain active because the NonLogin session has
* not yet expired).
*
* Try to access the protected resource again, before the SSO session
* has expired.
* This should be successful with SC_OK 200 status.
*
* Finally, wait for the non-login session to expire and try again..
* This should be rejected with SC_FORBIDDEN 403 status.
*
* (see bugfix https://bz.apache.org/bugzilla/show_bug.cgi?id=52303)
*/
@Test
public void testDigestExpiredAcceptProtectedWithCookies() throws Exception {
doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED,
true, 401, true, true, NC1, CNONCE, QOP, true);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
true, false, 200);
// wait long enough for the BASIC session to expire,
// but not long enough for NonLogin session expiry
Thread.sleep(SHORT_TIMEOUT_DELAY_MSECS);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
true, false, 200);
// wait long enough for my NonLogin session to expire
// and tear down the SSO session at the same time.
Thread.sleep(LONG_TIMEOUT_DELAY_MSECS);
doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED,
false, true, 403);
}
public void doTestNonLogin(String uri, boolean addCookies,
boolean expectedReject, int expectedRC)
throws Exception {
Map<String,List<String>> reqHeaders = new HashMap<>();
Map<String,List<String>> respHeaders = new HashMap<>();
ByteChunk bc = new ByteChunk();
if (addCookies) {
addCookies(reqHeaders);
}
int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders,
respHeaders);
if (expectedReject) {
Assert.assertEquals(expectedRC, rc);
Assert.assertTrue(bc.getLength() > 0);
}
else {
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
saveCookies(respHeaders);
}
}
public void doTestDigest(String user, String pwd, String uri,
boolean expectedReject1, int expectedRC1,
boolean useServerNonce, boolean useServerOpaque,
String nc1, String cnonce,
String qop, boolean req2expect200)
throws Exception {
String digestUri= uri;
List<String> auth = new ArrayList<>();
Map<String,List<String>> reqHeaders1 = new HashMap<>();
Map<String,List<String>> respHeaders1 = new HashMap<>();
// the first access attempt should be challenged
auth.add(buildDigestResponse(user, pwd, digestUri, REALM, "null",
"null", nc1, cnonce, qop));
reqHeaders1.put(CLIENT_AUTH_HEADER, auth);
ByteChunk bc = new ByteChunk();
int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders1,
respHeaders1);
if (expectedReject1) {
Assert.assertEquals(expectedRC1, rc);
Assert.assertTrue(bc.getLength() > 0);
}
else {
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
saveCookies(respHeaders1);
return;
}
// Second request should succeed (if we use the server nonce)
Map<String,List<String>> reqHeaders2 = new HashMap<>();
Map<String,List<String>> respHeaders2 = new HashMap<>();
auth.clear();
if (useServerNonce) {
if (useServerOpaque) {
auth.add(buildDigestResponse(user, pwd, digestUri,
getAuthToken(respHeaders1, REALM),
getAuthToken(respHeaders1, NONCE),
getAuthToken(respHeaders1, OPAQUE),
nc1, cnonce, qop));
} else {
auth.add(buildDigestResponse(user, pwd, digestUri,
getAuthToken(respHeaders1, REALM),
getAuthToken(respHeaders1, NONCE),
"null", nc1, cnonce, qop));
}
} else {
auth.add(buildDigestResponse(user, pwd, digestUri,
getAuthToken(respHeaders2, REALM),
"null", getAuthToken(respHeaders1, OPAQUE),
nc1, cnonce, QOP));
}
reqHeaders2.put(CLIENT_AUTH_HEADER, auth);
bc.recycle();
rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders2,
respHeaders2);
if (req2expect200) {
Assert.assertEquals(200, rc);
Assert.assertEquals("OK", bc.toString());
saveCookies(respHeaders2);
} else {
Assert.assertEquals(401, rc);
Assert.assertTrue((bc.getLength() > 0));
}
}
@Override
public void setUp() throws Exception {
super.setUp();
// create a tomcat server using the default in-memory Realm
Tomcat tomcat = getTomcatInstance();
// associate the SingeSignOn Valve before the Contexts
SingleSignOn sso = new SingleSignOn();
tomcat.getHost().getPipeline().addValve(sso);
// add the test user and role to the Realm
tomcat.addUser(USER, PWD);
tomcat.addRole(USER, ROLE);
// setup both NonLogin, Login and digest webapps
setUpNonLogin(tomcat);
setUpDigest(tomcat);
tomcat.start();
}
private void setUpNonLogin(Tomcat tomcat) throws Exception {
// Must have a real docBase for webapps - just use temp
Context ctxt = tomcat.addContext(CONTEXT_PATH_NOLOGIN,
System.getProperty("java.io.tmpdir"));
ctxt.setSessionTimeout(LONG_TIMEOUT_SECS);
// Add protected servlet
Tomcat.addServlet(ctxt, "TesterServlet1", new TesterServlet());
ctxt.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1");
SecurityCollection collection1 = new SecurityCollection();
collection1.addPatternDecoded(URI_PROTECTED);
SecurityConstraint sc1 = new SecurityConstraint();
sc1.addAuthRole(ROLE);
sc1.addCollection(collection1);
ctxt.addConstraint(sc1);
// Add unprotected servlet
Tomcat.addServlet(ctxt, "TesterServlet2", new TesterServlet());
ctxt.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2");
SecurityCollection collection2 = new SecurityCollection();
collection2.addPatternDecoded(URI_PUBLIC);
SecurityConstraint sc2 = new SecurityConstraint();
// do not add a role - which signals access permitted without one
sc2.addCollection(collection2);
ctxt.addConstraint(sc2);
// Configure the appropriate authenticator
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("NONE");
ctxt.setLoginConfig(lc);
ctxt.getPipeline().addValve(new NonLoginAuthenticator());
}
private void setUpDigest(Tomcat tomcat) throws Exception {
// Must have a real docBase for webapps - just use temp
Context ctxt = tomcat.addContext(CONTEXT_PATH_DIGEST,
System.getProperty("java.io.tmpdir"));
ctxt.setSessionTimeout(SHORT_TIMEOUT_SECS);
// Add protected servlet
Tomcat.addServlet(ctxt, "TesterServlet3", new TesterServlet());
ctxt.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3");
SecurityCollection collection = new SecurityCollection();
collection.addPatternDecoded(URI_PROTECTED);
SecurityConstraint sc = new SecurityConstraint();
sc.addAuthRole(ROLE);
sc.addCollection(collection);
ctxt.addConstraint(sc);
// Configure the appropriate authenticator
LoginConfig lc = new LoginConfig();
lc.setAuthMethod("DIGEST");
ctxt.setLoginConfig(lc);
ctxt.getPipeline().addValve(new DigestAuthenticator());
}
protected static String getAuthToken(
Map<String,List<String>> respHeaders, String token) {
final String AUTH_PREFIX = "=\"";
final String AUTH_SUFFIX = "\"";
List<String> authHeaders =
respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME);
// Assume there is only one
String authHeader = authHeaders.iterator().next();
String searchFor = token + AUTH_PREFIX;
int start = authHeader.indexOf(searchFor) + searchFor.length();
int end = authHeader.indexOf(AUTH_SUFFIX, 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);
}
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()));
}
/*
* extract and save the server cookies from the incoming response
*/
protected void saveCookies(Map<String,List<String>> respHeaders) {
// we only save the Cookie values, not header prefix
List<String> cookieHeaders = respHeaders.get(SERVER_COOKIES);
if (cookieHeaders == null) {
cookies = null;
} else {
cookies = new ArrayList<>(cookieHeaders.size());
for (String cookieHeader : cookieHeaders) {
cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';')));
}
}
}
/*
* add all saved cookies to the outgoing request
*/
protected void addCookies(Map<String,List<String>> reqHeaders) {
if ((cookies != null) && (cookies.size() > 0)) {
StringBuilder cookieHeader = new StringBuilder();
boolean first = true;
for (String cookie : cookies) {
if (!first) {
cookieHeader.append(';');
} else {
first = false;
}
cookieHeader.append(cookie);
}
List<String> cookieHeaderList = new ArrayList<>(1);
cookieHeaderList.add(cookieHeader.toString());
reqHeaders.put(BROWSER_COOKIES, cookieHeaderList);
}
}
}

View File

@@ -0,0 +1,274 @@
/*
* 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.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.filters.TesterHttpServletResponse;
import org.apache.catalina.startup.TesterMapRealm;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.security.ConcurrentMessageDigest;
import org.apache.tomcat.util.security.MD5Encoder;
public class TesterDigestAuthenticatorPerformance {
private static String USER = "user";
private static String PWD = "pwd";
private static String ROLE = "role";
private static String METHOD = "GET";
private static String URI = "/protected";
private static String CONTEXT_PATH = "/foo";
private static String CLIENT_AUTH_HEADER = "authorization";
private static String REALM = "TestRealm";
private static String QOP = "auth";
private static final AtomicInteger nonceCount = new AtomicInteger(0);
private DigestAuthenticator authenticator = new DigestAuthenticator();
@Test
public void testSimple() throws Exception {
doTest(4, 1000000);
}
public void doTest(int threadCount, int requestCount) throws Exception {
TesterRunnable runnables[] = new TesterRunnable[threadCount];
Thread threads[] = new Thread[threadCount];
String nonce = authenticator.generateNonce(new TesterDigestRequest());
// Create the runnables & threads
for (int i = 0; i < threadCount; i++) {
runnables[i] =
new TesterRunnable(authenticator, nonce, requestCount);
threads[i] = new Thread(runnables[i]);
}
long start = System.currentTimeMillis();
// Start the threads
for (int i = 0; i < threadCount; i++) {
threads[i].start();
}
// Wait for the threads to finish
for (int i = 0; i < threadCount; i++) {
threads[i].join();
}
double wallTime = System.currentTimeMillis() - start;
// Gather the results...
double totalTime = 0;
int totalSuccess = 0;
for (int i = 0; i < threadCount; i++) {
System.out.println("Thread: " + i + " Success: " +
runnables[i].getSuccess());
totalSuccess = totalSuccess + runnables[i].getSuccess();
totalTime = totalTime + runnables[i].getTime();
}
System.out.println("Average time per request (user): " +
totalTime/(threadCount * requestCount));
System.out.println("Average time per request (wall): " +
wallTime/(threadCount * requestCount));
Assert.assertEquals(((long)requestCount) * threadCount, totalSuccess);
}
@Before
public void setUp() throws Exception {
ConcurrentMessageDigest.init("MD5");
// Configure the Realm
TesterMapRealm realm = new TesterMapRealm();
realm.addUser(USER, PWD);
realm.addUserRole(USER, ROLE);
// Add the Realm to the Context
Context context = new StandardContext();
context.setName(CONTEXT_PATH);
context.setRealm(realm);
// Configure the Login config
LoginConfig config = new LoginConfig();
config.setRealmName(REALM);
context.setLoginConfig(config);
// Make the Context and Realm visible to the Authenticator
authenticator.setContainer(context);
authenticator.setNonceCountWindowSize(8 * 1024);
authenticator.start();
}
private static class TesterRunnable implements Runnable {
private String nonce;
private int requestCount;
private int success = 0;
private long time = 0;
private TesterDigestRequest request;
private HttpServletResponse response;
private DigestAuthenticator authenticator;
private static final String A1 = USER + ":" + REALM + ":" + PWD;
private static final String A2 = METHOD + ":" + CONTEXT_PATH + URI;
private static final String MD5A1 = MD5Encoder.encode(
ConcurrentMessageDigest.digest("MD5", A1.getBytes()));
private static final String MD5A2 = MD5Encoder.encode(
ConcurrentMessageDigest.digest("MD5", A2.getBytes()));
// All init code should be in here. run() needs to be quick
public TesterRunnable(DigestAuthenticator authenticator,
String nonce, int requestCount) throws Exception {
this.authenticator = authenticator;
this.nonce = nonce;
this.requestCount = requestCount;
request = new TesterDigestRequest();
request.getMappingData().context = authenticator.context;
response = new TesterHttpServletResponse();
}
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < requestCount; i++) {
try {
request.setAuthHeader(buildDigestResponse(nonce));
if (authenticator.authenticate(request, response)) {
success++;
}
// Clear out authenticated user ready for next iteration
request.setUserPrincipal(null);
} catch (IOException ioe) {
// Ignore
}
}
time = System.currentTimeMillis() - start;
}
public int getSuccess() {
return success;
}
public long getTime() {
return time;
}
private String buildDigestResponse(String nonce) {
String ncString = String.format("%1$08x",
Integer.valueOf(nonceCount.incrementAndGet()));
String cnonce = "cnonce";
String response = MD5A1 + ":" + nonce + ":" + ncString + ":" +
cnonce + ":" + QOP + ":" + MD5A2;
String md5response = MD5Encoder.encode(
ConcurrentMessageDigest.digest("MD5", response.getBytes()));
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(CONTEXT_PATH + URI);
auth.append("\", opaque=\"");
auth.append(authenticator.getOpaque());
auth.append("\", response=\"");
auth.append(md5response);
auth.append("\"");
auth.append(", qop=");
auth.append(QOP);
auth.append(", nc=");
auth.append(ncString);
auth.append(", cnonce=\"");
auth.append(cnonce);
auth.append("\"");
return auth.toString();
}
}
private static class TesterDigestRequest extends Request {
private String authHeader = null;
@Override
public String getRemoteAddr() {
return "127.0.0.1";
}
public void setAuthHeader(String authHeader) {
this.authHeader = authHeader;
}
@Override
public String getHeader(String name) {
if (CLIENT_AUTH_HEADER.equalsIgnoreCase(name)) {
return authHeader;
} else {
return super.getHeader(name);
}
}
@Override
public String getMethod() {
return METHOD;
}
@Override
public String getQueryString() {
return null;
}
@Override
public String getRequestURI() {
return CONTEXT_PATH + URI;
}
@Override
public org.apache.coyote.Request getCoyoteRequest() {
return new org.apache.coyote.Request();
}
}
}

View File

@@ -0,0 +1,446 @@
/**
* 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.jaspic;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.security.auth.message.config.AuthConfigFactory;
import javax.security.auth.message.config.AuthConfigProvider;
import javax.security.auth.message.config.RegistrationListener;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.apache.catalina.Globals;
public class TestAuthConfigFactoryImpl {
private String oldCatalinaBase;
private static final File TEST_CONFIG_FILE = new File("test/conf/jaspic-providers.xml");
@Test
public void testRegistrationNullLayer() {
doTestResistration(null, "AC_1", ":AC_1");
}
@Test
public void testRegistrationNullAppContext() {
doTestResistration("L_1", null, "L_1:");
}
@Test
public void testRegistrationNullLayerAndNullAppContext() {
doTestResistration(null, null, ":");
}
@Test
public void testSearchNoMatch01() {
doTestSearchOrder("foo", "bar", 1);
}
@Test
public void testSearchNoMatch02() {
doTestSearchOrder(null, "bar", 1);
}
@Test
public void testSearchNoMatch03() {
doTestSearchOrder("foo", null, 1);
}
@Test
public void testSearchNoMatch04() {
doTestSearchOrder(null, null, 1);
}
@Test
public void testSearchOnlyAppContextMatch01() {
doTestSearchOrder("foo", "AC_1", 2);
}
@Test
public void testSearchOnlyAppContextMatch02() {
doTestSearchOrder(null, "AC_1", 2);
}
@Test
public void testSearchOnlyAppContextMatch03() {
doTestSearchOrder("L_2", "AC_1", 2);
}
@Test
public void testSearchOnlyLayerMatch01() {
doTestSearchOrder("L_1", "bar", 3);
}
@Test
public void testSearchOnlyLayerMatch02() {
doTestSearchOrder("L_1", null, 3);
}
@Test
public void testSearchOnlyLayerMatch03() {
doTestSearchOrder("L_1", "AC_2", 3);
}
@Test
public void testSearchBothMatch() {
doTestSearchOrder("L_2", "AC_2", 4);
}
private void doTestSearchOrder(String layer, String appContext, int expected) {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp1, null, null, "1");
AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp2, null, "AC_1", "2");
AuthConfigProvider acp3 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp3, "L_1", null, "3");
AuthConfigProvider acp4 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp4, "L_2", "AC_2", "4");
AuthConfigProvider searchResult = factory.getConfigProvider(layer, appContext, null);
int searchIndex;
if (searchResult == acp1) {
searchIndex = 1;
} else if (searchResult == acp2) {
searchIndex = 2;
} else if (searchResult == acp3) {
searchIndex = 3;
} else if (searchResult == acp4) {
searchIndex = 4;
} else {
searchIndex = -1;
}
Assert.assertEquals(expected, searchIndex);
}
private void doTestResistration(String layer, String appContext, String expectedRegId) {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
SimpleRegistrationListener listener = new SimpleRegistrationListener(layer, appContext);
String regId = factory.registerConfigProvider(acp1, layer, appContext, null);
Assert.assertEquals(expectedRegId, regId);
factory.getConfigProvider(layer, appContext, listener);
factory.removeRegistration(regId);
Assert.assertTrue(listener.wasCorrectlyCalled());
listener.reset();
factory.registerConfigProvider(acp1, layer, appContext, null);
factory.getConfigProvider(layer, appContext, listener);
// Replace it
AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp2, layer, appContext, null);
Assert.assertTrue(listener.wasCorrectlyCalled());
}
@Test
public void testRegistrationInsertExact01() {
doTestRegistrationInsert("L_3", "AC_2", "L_3", "AC_2");
}
@Test
public void testRegistrationInsertExact02() {
doTestRegistrationInsert("L_2", "AC_3", "L_2", "AC_3");
}
@Test
public void testRegistrationInsertExact03() {
doTestRegistrationInsert("L_4", "AC_4", "L_4", "AC_4");
}
@Test
public void testRegistrationInsertAppContext01() {
doTestRegistrationInsert(null, "AC_3", "L_2", "AC_3");
}
@Test
public void testRegistrationInsertAppContext02() {
doTestRegistrationInsert(null, "AC_4", "L_4", "AC_4");
}
@Test
public void testRegistrationInsertLayer01() {
doTestRegistrationInsert("L_4", null, "L_4", "AC_4");
}
private void doTestRegistrationInsert(String newLayer, String newAppContext,
String expectedListenerLayer, String expectedListenerAppContext) {
// Set up
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp1, "L_1", "AC_1", null);
AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp2, null, "AC_2", null);
AuthConfigProvider acp3 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp3, "L_2", null, null);
AuthConfigProvider acp4 = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acp4, null, null, null);
SimpleRegistrationListener listener1 = new SimpleRegistrationListener("L_1", "AC_1");
factory.getConfigProvider("L_1", "AC_1", listener1);
SimpleRegistrationListener listener2 = new SimpleRegistrationListener("L_3", "AC_2");
factory.getConfigProvider("L_3", "AC_2", listener2);
SimpleRegistrationListener listener3 = new SimpleRegistrationListener("L_2", "AC_3");
factory.getConfigProvider("L_2", "AC_3", listener3);
SimpleRegistrationListener listener4 = new SimpleRegistrationListener("L_4", "AC_4");
factory.getConfigProvider("L_4", "AC_4", listener4);
List<SimpleRegistrationListener> listeners = new ArrayList<>();
listeners.add(listener1);
listeners.add(listener2);
listeners.add(listener3);
listeners.add(listener4);
// Register a new provider that will impact some existing registrations
AuthConfigProvider acpNew = new SimpleAuthConfigProvider(null, null);
factory.registerConfigProvider(acpNew, newLayer, newAppContext, null);
// Check to see if the expected listener fired.
for (SimpleRegistrationListener listener : listeners) {
if (listener.wasCalled()) {
Assert.assertEquals(listener.layer, expectedListenerLayer);
Assert.assertEquals(listener.appContext, expectedListenerAppContext);
Assert.assertTrue(listener.wasCorrectlyCalled());
} else {
Assert.assertFalse((listener.layer.equals(expectedListenerLayer) &&
listener.appContext.equals(expectedListenerAppContext)));
}
}
}
@Test
public void testDetachListenerNonexistingRegistration() {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
String registrationId = factory.registerConfigProvider(acp1, "L_1", "AC_1", null);
SimpleRegistrationListener listener1 = new SimpleRegistrationListener("L_1", "AC_1");
factory.getConfigProvider("L_1", "AC_1", listener1);
factory.removeRegistration(registrationId);
String[] registrationIds = factory.detachListener(listener1, "L_1", "AC_1");
Assert.assertTrue(registrationIds.length == 0);
}
@Test
public void testDetachListener() {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
String registrationId = factory.registerConfigProvider(acp1, "L_1", "AC_1", null);
SimpleRegistrationListener listener1 = new SimpleRegistrationListener("L_1", "AC_1");
factory.getConfigProvider("L_1", "AC_1", listener1);
String[] registrationIds = factory.detachListener(listener1, "L_1", "AC_1");
Assert.assertTrue(registrationIds.length == 1);
Assert.assertEquals(registrationId, registrationIds[0]);
}
@Test
public void testRegistrationNullListener() {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
String registrationId = factory.registerConfigProvider(acp1, "L_1", "AC_1", null);
factory.getConfigProvider("L_1", "AC_1", null);
boolean result = factory.removeRegistration(registrationId);
Assert.assertTrue(result);
}
@Test
public void testAllRegistrationIds() {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null);
String registrationId1 = factory.registerConfigProvider(acp1, "L_1", "AC_1", null);
AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null);
String registrationId2 = factory.registerConfigProvider(acp2, "L_2", "AC_2", null);
String[] registrationIds = factory.getRegistrationIDs(null);
Assert.assertTrue(registrationIds.length == 2);
Set<String> ids = new HashSet<>(Arrays.asList(registrationIds));
Assert.assertTrue(ids.contains(registrationId1));
Assert.assertTrue(ids.contains(registrationId2));
}
@Before
public void setUp() {
// set CATALINA_BASE to test so that the file with persistent providers will be written in test/conf folder
oldCatalinaBase = System.getProperty(Globals.CATALINA_BASE_PROP);
System.setProperty(Globals.CATALINA_BASE_PROP, "test");
if (TEST_CONFIG_FILE.exists()) {
if (!TEST_CONFIG_FILE.delete()) {
Assert.fail("Failed to delete " + TEST_CONFIG_FILE);
}
}
}
@After
public void cleanUp() {
if (oldCatalinaBase != null ) {
System.setProperty(Globals.CATALINA_BASE_PROP, oldCatalinaBase);
} else {
System.clearProperty(Globals.CATALINA_BASE_PROP);
}
if (TEST_CONFIG_FILE.exists()) {
if (!TEST_CONFIG_FILE.delete()) {
Assert.fail("Failed to delete " + TEST_CONFIG_FILE);
}
}
}
@Test
public void testRemovePersistentRegistration() {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
factory.registerConfigProvider(
SimpleAuthConfigProvider.class.getName(), null, "L_1", "AC_1", null);
String registrationId2 = factory.registerConfigProvider(
SimpleAuthConfigProvider.class.getName(), null, "L_2", "AC_2", null);
factory.removeRegistration(registrationId2);
factory.refresh();
String[] registrationIds = factory.getRegistrationIDs(null);
for (String registrationId : registrationIds) {
Assert.assertNotEquals(registrationId2, registrationId);
}
}
@Test
public void testRegistrationNullClassName() {
doTestNullClassName(false, "L_1", "AC_1");
}
@Test
public void testRegistrationNullClassOverrideExisting() {
doTestNullClassName(true, "L_1", "AC_1");
}
@Test
public void testRegistrationNullClassNullLayerNullAppContext() {
doTestNullClassName(false, null, null);
}
private void doTestNullClassName(boolean shouldOverrideExistingProvider, String layer, String appContext) {
AuthConfigFactory factory = new AuthConfigFactoryImpl();
if (shouldOverrideExistingProvider) {
factory.registerConfigProvider(SimpleAuthConfigProvider.class.getName(), null, layer, appContext, null);
}
String registrationId = factory.registerConfigProvider(null, null, layer, appContext, null);
factory.refresh();
String[] registrationIds = factory.getRegistrationIDs(null);
Set<String> ids = new HashSet<>(Arrays.asList(registrationIds));
Assert.assertTrue(ids.contains(registrationId));
AuthConfigProvider provider = factory.getConfigProvider(layer, appContext, null);
Assert.assertNull(provider);
}
private static class SimpleRegistrationListener implements RegistrationListener {
private final String layer;
private final String appContext;
private boolean called = false;
private String layerNotified;
private String appContextNotified;
public SimpleRegistrationListener(String layer, String appContext) {
this.layer = layer;
this.appContext = appContext;
}
@Override
public void notify(String layer, String appContext) {
called = true;
layerNotified = layer;
appContextNotified = appContext;
}
public boolean wasCalled() {
return called;
}
public boolean wasCorrectlyCalled() {
return called && areTheSame(layer, layerNotified) &&
areTheSame(appContext, appContextNotified);
}
public void reset() {
called = false;
layerNotified = null;
appContextNotified = null;
}
private static boolean areTheSame(String a, String b) {
if (a == null) {
return b == null;
}
return a.equals(b);
}
}
}

View File

@@ -0,0 +1,135 @@
/**
* 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.jaspic;
import java.io.File;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations.Provider;
import org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations.Providers;
public class TestPersistentProviderRegistrations {
@Test
public void testLoadEmpty() {
File f = new File("test/conf/jaspic-test-01.xml");
Providers result = PersistentProviderRegistrations.loadProviders(f);
Assert.assertEquals(0, result.getProviders().size());
}
@Test
public void testLoadSimple() {
File f = new File("test/conf/jaspic-test-02.xml");
Providers result = PersistentProviderRegistrations.loadProviders(f);
validateSimple(result);
}
private void validateSimple(Providers providers) {
Assert.assertEquals(1, providers.getProviders().size());
Provider p = providers.getProviders().get(0);
Assert.assertEquals("a", p.getClassName());
Assert.assertEquals("b", p.getLayer());
Assert.assertEquals("c", p.getAppContext());
Assert.assertEquals("d", p.getDescription());
Assert.assertEquals(2, p.getProperties().size());
Assert.assertEquals("f", p.getProperties().get("e"));
Assert.assertEquals("h", p.getProperties().get("g"));
}
@Test
public void testSaveSimple() {
File f = new File("test/conf/jaspic-test-03.xml");
if (f.exists()) {
Assert.assertTrue(f.delete());
}
// Create a config and write it out
Providers start = new Providers();
Provider p = new Provider();
p.setClassName("a");
p.setLayer("b");
p.setAppContext("c");
p.setDescription("d");
p.addProperty("e", "f");
p.addProperty("g", "h");
start.addProvider(p);
PersistentProviderRegistrations.writeProviders(start, f);
// Read it back
Providers end = PersistentProviderRegistrations.loadProviders(f);
validateSimple(end);
if (f.exists()) {
Assert.assertTrue("Failed to clean up [" + f + "]", f.delete());
}
}
@Test
public void testLoadProviderWithoutLayerAndAC() {
File f = new File("test/conf/jaspic-test-04.xml");
Providers providers = PersistentProviderRegistrations.loadProviders(f);
validateNoLayerAndAC(providers);
}
private void validateNoLayerAndAC(Providers providers) {
Assert.assertEquals(1, providers.getProviders().size());
Provider p = providers.getProviders().get(0);
Assert.assertEquals("a", p.getClassName());
Assert.assertNull(p.getLayer());
Assert.assertNull(p.getAppContext());
Assert.assertEquals("d", p.getDescription());
}
@Test
public void testSaveProviderWithoutLayerAndAC() {
File f = new File("test/conf/jaspic-test-05.xml");
if (f.exists()) {
Assert.assertTrue(f.delete());
}
// Create a config and write it out
Providers initialProviders = new Providers();
Provider p = new Provider();
p.setClassName("a");
p.setDescription("d");
initialProviders.addProvider(p);
PersistentProviderRegistrations.writeProviders(initialProviders, f);
// Read it back
Providers loadedProviders = PersistentProviderRegistrations.loadProviders(f);
try {
validateNoLayerAndAC(loadedProviders);
} finally {
if (f.exists()) {
if (!f.delete()) {
Assert.fail("Failed to delete " + f);
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.jaspic;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.message.AuthException;
import javax.security.auth.message.MessageInfo;
import javax.security.auth.message.config.ServerAuthConfig;
import javax.security.auth.message.config.ServerAuthContext;
import org.junit.Assert;
import org.junit.Test;
public class TestSimpleServerAuthConfig {
private static final String SERVER_AUTH_MODULE_KEY_PREFIX =
"org.apache.catalina.authenticator.jaspic.ServerAuthModule.";
private static final Map<String,String> CONFIG_PROPERTIES;
static {
CONFIG_PROPERTIES = new HashMap<>();
CONFIG_PROPERTIES.put(SERVER_AUTH_MODULE_KEY_PREFIX + "1",
TesterServerAuthModuleA.class.getName());
}
@Test
public void testConfigOnServerAuthConfig() throws Exception {
ServerAuthConfig serverAuthConfig =
new SimpleServerAuthConfig(null, null, null, CONFIG_PROPERTIES);
ServerAuthContext serverAuthContext = serverAuthConfig.getAuthContext(null, null, null);
validateServerAuthContext(serverAuthContext);
}
@Test
public void testConfigOnGetAuthContext() throws Exception {
ServerAuthConfig serverAuthConfig = new SimpleServerAuthConfig(null, null, null, null);
ServerAuthContext serverAuthContext =
serverAuthConfig.getAuthContext(null, null, CONFIG_PROPERTIES);
validateServerAuthContext(serverAuthContext);
}
@Test(expected=AuthException.class)
public void testConfigNone() throws Exception {
ServerAuthConfig serverAuthConfig = new SimpleServerAuthConfig(null, null, null, null);
serverAuthConfig.getAuthContext(null, null, null);
}
private void validateServerAuthContext(ServerAuthContext serverAuthContext) throws Exception {
MessageInfo msgInfo = new TesterMessageInfo();
serverAuthContext.cleanSubject(msgInfo, null);
Assert.assertEquals("init()-cleanSubject()-", msgInfo.getMap().get("trace"));
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.jaspic;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.message.MessageInfo;
public class TesterMessageInfo implements MessageInfo {
private Object requestMessage;
private Object responseMessage;
private final Map<String,String> map = new HashMap<>();
@Override
public Object getRequestMessage() {
return requestMessage;
}
@Override
public Object getResponseMessage() {
return responseMessage;
}
@Override
public void setRequestMessage(Object request) {
requestMessage = request;
}
@Override
public void setResponseMessage(Object response) {
responseMessage = response;
}
@SuppressWarnings("rawtypes")
@Override
public Map getMap() {
return map;
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.jaspic;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.message.AuthException;
import javax.security.auth.message.AuthStatus;
import javax.security.auth.message.MessageInfo;
import javax.security.auth.message.MessagePolicy;
import javax.security.auth.message.module.ServerAuthModule;
public class TesterServerAuthModuleA implements ServerAuthModule {
private StringBuilder trace = new StringBuilder("init()-");
@Override
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject,
Subject serviceSubject) throws AuthException {
return null;
}
@Override
public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject)
throws AuthException {
return null;
}
@SuppressWarnings("unchecked")
@Override
public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException {
trace.append("cleanSubject()-");
messageInfo.getMap().put("trace", trace.toString());
}
@Override
public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy,
CallbackHandler handler, @SuppressWarnings("rawtypes") Map options)
throws AuthException {
// NO-OP
}
@SuppressWarnings("rawtypes")
@Override
public Class[] getSupportedMessageTypes() {
return null;
}
}