Index: test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java =================================================================== --- test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java (revision 1555554) +++ test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java (working copy) @@ -22,14 +22,19 @@ import java.util.List; import java.util.Map; +import javax.servlet.http.HttpServletResponse; + import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.junit.Test; import org.apache.catalina.Context; -import org.apache.catalina.startup.TesterServlet; +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; @@ -47,12 +52,36 @@ * 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. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * 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 requires 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"; @@ -60,216 +89,370 @@ 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 + 5) * 1000); + // 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; - private static String CLIENT_AUTH_HEADER = "authorization"; - private static String SERVER_COOKIES = "Set-Cookie"; - private static String BROWSER_COOKIES = "Cookie"; + // 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 long REASONABLE_MSECS_TO_EXPIRY = + (((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 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 cookies; + private String encodedURL; /* - * Try to access an unprotected resource without an established - * SSO session. - * This should be permitted. + * Run some sanity checks without an established SSO session + * to make sure the test environment is correct. */ @Test - public void testAcceptPublicNonLogin() throws Exception { + public void testEssentialEnvironment() throws Exception { + + // should be permitted to access an unprotected resource. doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, - false, false, 200); + 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); } - /* - * 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 { + 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, - false, true, 403); + 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, which - * will then be followed by successful re-authentication. + * the resource. This should be rejected with SC_FORBIDDEN 401 status. + * + * Note: this test will run for slightly more than 1 minute. */ @Test - public void testBasicLoginSessionTimeout() throws Exception { - doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - true, 401, false, 200); - // wait long enough for my session to expire - Thread.sleep(SHORT_TIMEOUT_DELAY_MSECS); - doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - true, 401, false, 200); + 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, but without sending the SSO session cookie. - * This should be rejected with SC_FORBIDDEN 403 status. + * webapp while providing the SSO session cookie received from the + * first webapp. This should be successful with SC_OK 200 status. */ @Test - public void testBasicLoginRejectProtectedWithoutCookies() throws Exception { - doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - true, 401, false, 200); + 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, - false, true, 403); + 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 while sending the SSO session cookie provided by the - * first webapp. - * This should be successful with SC_OK 200 status. + * webapp, but without sending the SSO session cookie. + * This should be rejected with SC_FORBIDDEN 403 status. */ @Test - public void testBasicLoginAcceptProtectedWithCookies() throws Exception { - doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - true, 401, false, 200); + 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, - true, false, 200); + 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; + 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; + assertEquals(firstEncodedURL, secondEncodedURL); + 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 sending the SSO session cookie provided by the - * first webapp. - * This should be successful with SC_OK 200 status. + * 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. + * 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://issues.apache.org/bugzilla/show_bug.cgi?id=52303) + * + * Note: this test will run for slightly more than 3 minutes. */ @Test public void testBasicExpiredAcceptProtectedWithCookies() throws Exception { - doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - true, 401, false, 200); + + 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, - true, false, 200); + USE_COOKIES, HttpServletResponse.SC_OK); // wait long enough for the BASIC session to expire, - // but not long enough for NonLogin session expiry - Thread.sleep(SHORT_TIMEOUT_DELAY_MSECS); + // 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, - true, false, 200); + USE_COOKIES, HttpServletResponse.SC_OK); - // 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); + // 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 addCookies, - boolean expectedReject, int expectedRC) - throws Exception { + public void doTestNonLogin(String uri, boolean useCookie, + int expectedRC) throws Exception { Map> reqHeaders = new HashMap<>(); - if (addCookies) { - addCookies(reqHeaders); - } Map> respHeaders = new HashMap<>(); + if (useCookie && (cookies != null)) { + reqHeaders.put(CLIENT_COOKIE_HEADER + ":", cookies); + } + ByteChunk bc = new ByteChunk(); int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, respHeaders); - if (expectedReject) { + if (expectedRC != HttpServletResponse.SC_OK) { assertEquals(expectedRC, rc); assertTrue(bc.getLength() > 0); } else { - assertEquals(200, rc); assertEquals("OK", bc.toString()); - saveCookies(respHeaders); } } - public void doTestBasic(String user, String pwd, String uri, - boolean expectedReject1, int expectedRC1, - boolean expectedReject2, int expectedRC2) throws Exception { + private void doTestBasic(String uri, + TestSSOnonLoginAndBasicAuthenticator.BasicCredentials credentials, + boolean useCookie, int expectedRC) throws Exception { - // the first access attempt should be challenged - Map> reqHeaders1 = new HashMap<>(); - Map> respHeaders1 = new HashMap<>(); + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); - ByteChunk bc = new ByteChunk(); - int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders1, - respHeaders1); - - if (expectedReject1) { - assertEquals(expectedRC1, rc); - assertTrue(bc.getLength() > 0); + if (useCookie && (cookies != null)) { + reqHeaders.put(CLIENT_COOKIE_HEADER + ":", cookies); } else { - assertEquals(200, rc); - assertEquals("OK", bc.toString()); - return; + if (credentials != null) { + List auth = new ArrayList<>(); + auth.add(credentials.getCredentials()); + reqHeaders.put(CLIENT_AUTH_HEADER, auth); + } } - // the second access attempt should be successful - String credentials = user + ":" + pwd; + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); - String base64auth = Base64.encodeBase64String( - credentials.getBytes(StandardCharsets.ISO_8859_1)); - String authLine = "Basic " + base64auth; - - List auth = new ArrayList<>(); - auth.add(authLine); - Map> reqHeaders2 = new HashMap<>(); - reqHeaders2.put(CLIENT_AUTH_HEADER, auth); - - Map> respHeaders2 = new HashMap<>(); - - bc.recycle(); - rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders2, - respHeaders2); - - if (expectedReject2) { - assertEquals(expectedRC2, rc); - assertNull(bc.toString()); + assertEquals("Unexpected Return Code", expectedRC, rc); + if (expectedRC != HttpServletResponse.SC_OK) { + assertTrue(bc.getLength() > 0); + if (expectedRC == HttpServletResponse.SC_UNAUTHORIZED) { + // The server should identify the acceptable method(s) + boolean methodFound = false; + List authHeaders = respHeaders.get(SERVER_AUTH_HEADER); + for (String authHeader : authHeaders) { + if (authHeader.indexOf(NICE_METHOD) > -1) { + methodFound = true; + break; + } + } + assertTrue(methodFound); + } } else { - assertEquals(200, rc); - assertEquals("OK", bc.toString()); - saveCookies(respHeaders2); + String thePage = bc.toString(); + assertNotNull(thePage); + assertTrue(thePage.startsWith("OK")); + if (useCookie) { + List 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 = ""; + 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 tomcat = getTomcatInstance(); + tomcat = getTomcatInstance(); // associate the SingeSignOn Valve before the Contexts SingleSignOn sso = new SingleSignOn(); @@ -280,71 +463,92 @@ tomcat.addRole(USER, ROLE); // setup both NonLogin and Login webapps - setUpNonLogin(tomcat); - setUpLogin(tomcat); + setUpNonLogin(); + setUpLogin(); tomcat.start(); } - private void setUpNonLogin(Tomcat tomcat) throws Exception { + @Override + public void tearDown() throws Exception { + tomcat.stop(); + } + + private void setUpNonLogin() throws Exception { + // Must have a real docBase for webapps - just use temp - Context ctxt = tomcat.addContext(CONTEXT_PATH_NOLOGIN, + nonloginContext = tomcat.addContext(CONTEXT_PATH_NOLOGIN, System.getProperty("java.io.tmpdir")); - ctxt.setSessionTimeout(LONG_TIMEOUT_SECS); + nonloginContext.setSessionTimeout(LONG_SESSION_TIMEOUT_MINS); - // Add protected servlet - Tomcat.addServlet(ctxt, "TesterServlet1", new TesterServlet()); - ctxt.addServletMapping(URI_PROTECTED, "TesterServlet1"); + // Add protected servlet to the context + Tomcat.addServlet(nonloginContext, "TesterServlet1", + new TesterServletEncodeUrl()); + nonloginContext.addServletMapping(URI_PROTECTED, "TesterServlet1"); SecurityCollection collection1 = new SecurityCollection(); collection1.addPattern(URI_PROTECTED); SecurityConstraint sc1 = new SecurityConstraint(); sc1.addAuthRole(ROLE); sc1.addCollection(collection1); - ctxt.addConstraint(sc1); + nonloginContext.addConstraint(sc1); - // Add unprotected servlet - Tomcat.addServlet(ctxt, "TesterServlet2", new TesterServlet()); - ctxt.addServletMapping(URI_PUBLIC, "TesterServlet2"); + // Add unprotected servlet to the context + Tomcat.addServlet(nonloginContext, "TesterServlet2", + new TesterServletEncodeUrl()); + nonloginContext.addServletMapping(URI_PUBLIC, "TesterServlet2"); SecurityCollection collection2 = new SecurityCollection(); collection2.addPattern(URI_PUBLIC); SecurityConstraint sc2 = new SecurityConstraint(); // do not add a role - which signals access permitted without one sc2.addCollection(collection2); - ctxt.addConstraint(sc2); + nonloginContext.addConstraint(sc2); // Configure the authenticator and inherit the Realm from Engine LoginConfig lc = new LoginConfig(); lc.setAuthMethod("NONE"); - ctxt.setLoginConfig(lc); - ctxt.getPipeline().addValve(new NonLoginAuthenticator()); + nonloginContext.setLoginConfig(lc); + AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator(); + nonloginContext.getPipeline().addValve(nonloginAuthenticator); } - private void setUpLogin(Tomcat tomcat) throws Exception { + private void setUpLogin() throws Exception { // Must have a real docBase for webapps - just use temp - Context ctxt = tomcat.addContext(CONTEXT_PATH_LOGIN, + basicContext = tomcat.addContext(CONTEXT_PATH_LOGIN, System.getProperty("java.io.tmpdir")); - ctxt.setSessionTimeout(SHORT_TIMEOUT_SECS); + basicContext.setSessionTimeout(SHORT_SESSION_TIMEOUT_MINS); - // Add protected servlet - Tomcat.addServlet(ctxt, "TesterServlet3", new TesterServlet()); - ctxt.addServletMapping(URI_PROTECTED, "TesterServlet3"); - + // Add protected servlet to the context + Tomcat.addServlet(basicContext, "TesterServlet3", + new TesterServletEncodeUrl()); + basicContext.addServletMapping(URI_PROTECTED, "TesterServlet3"); SecurityCollection collection = new SecurityCollection(); collection.addPattern(URI_PROTECTED); SecurityConstraint sc = new SecurityConstraint(); sc.addAuthRole(ROLE); sc.addCollection(collection); - ctxt.addConstraint(sc); + basicContext.addConstraint(sc); - // Configure the appropriate authenticator + // Add unprotected servlet to the context + Tomcat.addServlet(basicContext, "TesterServlet4", + new TesterServletEncodeUrl()); + basicContext.addServletMapping(URI_PUBLIC, "TesterServlet4"); + SecurityCollection collection2 = new SecurityCollection(); + collection2.addPattern(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"); - ctxt.setLoginConfig(lc); - ctxt.getPipeline().addValve(new BasicAuthenticator()); + basicContext.setLoginConfig(lc); + AuthenticatorBase basicAuthenticator = new BasicAuthenticator(); + basicContext.getPipeline().addValve(basicAuthenticator); } /* @@ -353,7 +557,7 @@ protected void saveCookies(Map> respHeaders) { // we only save the Cookie values, not header prefix - cookies = respHeaders.get(SERVER_COOKIES); + cookies = respHeaders.get(SERVER_COOKIE_HEADER); } /* @@ -362,7 +566,90 @@ protected void addCookies(Map> reqHeaders) { if ((cookies != null) && (cookies.size() > 0)) { - reqHeaders.put(BROWSER_COOKIES + ":", cookies); + reqHeaders.put(CLIENT_COOKIE_HEADER + ":", cookies); } } -} \ No newline at end of file + + /* + * 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(REASONABLE_MSECS_TO_EXPIRY); + } + catch (InterruptedException ie) {/* ignored */}; + + // paranoid verification that active sessions have now gone + sessions = manager.findSessions(); + 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; + } + } +} Index: test/org/apache/catalina/startup/TesterServletEncodeUrl.java =================================================================== --- test/org/apache/catalina/startup/TesterServletEncodeUrl.java (working copy) +++ test/org/apache/catalina/startup/TesterServletEncodeUrl.java (working copy) @@ -24,10 +24,21 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class TesterServlet extends HttpServlet { +/** + * A test servlet that will always encode the url in case the client requires + * session persistence but is not configured to support cookies. + */ +public class TesterServletEncodeUrl extends HttpServlet { private static final long serialVersionUID = 1L; + /** + * Almost minimal processing for a servlet. + * + * @param nextUrl The url the caller would like to go to next. If + * supplied, put an encoded url into the returned + * html page as a hyperlink. + */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -35,5 +46,14 @@ resp.setContentType("text/plain"); PrintWriter out = resp.getWriter(); out.print("OK"); + + String param = req.getParameter("nextUrl"); + if (param!=null) { + // append an encoded url to carry the sessionids + String targetUrl = resp.encodeURL(param); + out.print(". You want to go here next."); + } } }