init
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
558
test/org/apache/catalina/authenticator/TestBasicAuthParser.java
Normal file
558
test/org/apache/catalina/authenticator/TestBasicAuthParser.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user