Index: TestNonLoginAndBasicAuthenticator.java =================================================================== --- TestNonLoginAndBasicAuthenticator.java (revision 1406101) +++ TestNonLoginAndBasicAuthenticator.java (working copy) @@ -48,6 +48,20 @@ */ public class TestNonLoginAndBasicAuthenticator 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 EXPECT_REJECT = true; + protected static final Boolean EXPECT_ACCEPT = !EXPECT_REJECT; + + protected static final Boolean USE_COOKIES = true; + protected static final Boolean NO_COOKIES = !USE_COOKIES; + + protected static final Boolean PROVIDE_CREDENTIALS = true; + protected static final Boolean NO_CREDENTIALS = !PROVIDE_CREDENTIALS; + + protected static final Boolean VERIFY_AUTH_SCHEME = true; + protected static final Boolean NO_VERIFY_AUTH_SCHEME = !VERIFY_AUTH_SCHEME; + private static final String USER = "user"; private static final String PWD = "pwd"; private static final String ROLE = "role"; @@ -58,13 +72,20 @@ 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 int LONG_TIMEOUT_SECS = 10; - private static final long LONG_TIMEOUT_DELAY_MSECS = - ((LONG_TIMEOUT_SECS + 2) * 1000); + private static final int SHORT_TIMEOUT_MINS = 1; + private static final int LONG_TIMEOUT_MINS = 2; + private static final long TIMEOUT_DELAY_MSECS = + (((SHORT_TIMEOUT_MINS * 60) + 10) * 1000); - private static String CLIENT_AUTH_HEADER = "authorization"; + private static final String CLIENT_AUTH_HEADER = "authorization"; + private static final String SERVER_COOKIE_HEADER = "Set-Cookie"; + private static final String CLIENT_COOKIE_HEADER = "Cookie"; + private Tomcat tomcat; + private AuthenticatorBase basicAuthenticator; + private AuthenticatorBase nonloginAuthenticator; + private List cookies; + /* * Try to access an unprotected resource in a webapp that * does not have a login method defined. @@ -72,7 +93,8 @@ */ @Test public void testAcceptPublicNonLogin() throws Exception { - doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, false, 200); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, + NO_COOKIES, EXPECT_ACCEPT, 200); } /* @@ -82,7 +104,8 @@ */ @Test public void testRejectProtectedNonLogin() throws Exception { - doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, true, 403); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + NO_COOKIES, EXPECT_REJECT, 403); } /* @@ -93,7 +116,8 @@ @Test public void testAcceptPublicBasic() throws Exception { doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PUBLIC, - false, false, 200, false, 200); + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); } /* @@ -104,7 +128,11 @@ @Test public void testAcceptProtectedBasic() throws Exception { doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - false, true, 401, false, 200); + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); } /* @@ -116,24 +144,123 @@ @Test public void testAuthMethodCaseBasic() throws Exception { doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - true, true, 401, false, 200); + VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); } /* + * Test the default behaviour of BASIC authentication, which does + * NOT create a session on the server. The authentication must be + * performed by the browser sending a valid authenticate header + * with every request for a protected resource. + * * Logon to access a protected resource in a webapp that uses - * BASIC authentication. Wait until that session times-out, - * then re-access the resource. - * This should be rejected with SC_FORBIDDEN 401 status, which - * can be followed by successful re-authentication. + * BASIC authentication. Immediately try to re-access the resource + * without providing credentials. This should be rejected with + * SC_FORBIDDEN 401 status, which can be followed by successful + * re-authentication. */ @Test + public void testBasicLoginWithoutSession() throws Exception { + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); + + // next, don't provide credentials initially + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); + } + + /* + * 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 AuthenticatorBase + * changeSessionIdOnAuthentication default 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(); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, USE_COOKIES, + EXPECT_ACCEPT, 200); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, USE_COOKIES, + EXPECT_ACCEPT, 200); + } + + /* + * Test the optional behaviour of BASIC authentication to create + * a session on the server. The server will return a session cookie. + * Verify the session timeout mechanism. + * + * 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. wait long enough for the session to time out. + * 5. try to access the protected resource once more using the + * original session cookie. It should be rejected as Unauthorized. + * 6. try to access the protected resource when providing credentials, + * so get OK status and a new server session cookie. + * 7. access the protected resource once more using the new session cookie. + */ + @Test public void testBasicLoginSessionTimeout() throws Exception { + setAlwaysUseSession(); doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - false, true, 401, false, 200); - // wait long enough for the session above to expire - Thread.sleep(LONG_TIMEOUT_DELAY_MSECS); + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - false, true, 401, false, 200); + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, USE_COOKIES, + EXPECT_ACCEPT, 200); + + // allow the session to time out and lose authentication + List originalCookies = cookies; + Thread.sleep(TIMEOUT_DELAY_MSECS); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, USE_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, USE_COOKIES, + EXPECT_ACCEPT, 200); + + // slightly paranoid verification + boolean sameCookies = originalCookies.equals(cookies); + assertTrue(!sameCookies); } /* @@ -146,18 +273,47 @@ @Test public void testBasicLoginRejectProtected() throws Exception { doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, - false, true, 401, false, 200); + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, - true, 403); + NO_COOKIES, EXPECT_REJECT, 403); } + /* + * Logon to access a protected resource in a webapp that uses + * BASIC authentication and session id. Then try to access a + * protected resource in a different webapp that does not have + * a login method using the original session. + * This should be rejected with SC_FORBIDDEN 403 status, confirming + * there has been no cross-authentication between the webapps. + */ + @Test + public void testBasicLoginRejectProtectedWithSession() throws Exception { + setAlwaysUseSession(); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, NO_CREDENTIALS, NO_COOKIES, + EXPECT_REJECT, 401); + doTestBasic(USER, PWD, CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_VERIFY_AUTH_SCHEME, PROVIDE_CREDENTIALS, NO_COOKIES, + EXPECT_ACCEPT, 200); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + USE_COOKIES, EXPECT_REJECT, 403); + } - private void doTestNonLogin(String uri, boolean expectedReject, - int expectedRC) throws Exception { + private void doTestNonLogin(String uri, boolean useCookie, + boolean expectedReject, int expectedRC) throws Exception { + Map> reqHeaders = new HashMap<>(); 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); @@ -173,84 +329,83 @@ } private void doTestBasic(String user, String pwd, String uri, - boolean verifyAuthSchemeCase, - boolean expectedReject1, int expectedRC1, - boolean expectedReject2, int expectedRC2) throws Exception { + boolean verifyAuthSchemeCase, boolean provideCredentials, + boolean useCookie, boolean expectedReject, 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 (provideCredentials) { + if (useCookie && (cookies != null)) { + reqHeaders.put(CLIENT_COOKIE_HEADER + ":", cookies); + } + else { + String credentials = user + ":" + pwd; + byte[] credentialsBytes = ByteChunk.convertToBytes(credentials); + String base64auth = Base64.encode(credentialsBytes); + String authScheme = verifyAuthSchemeCase ? "bAsIc " : "Basic "; + String authLine = authScheme + base64auth; - if (expectedReject1) { - assertEquals(expectedRC1, rc); - assertTrue(bc.getLength() > 0); + List auth = new ArrayList<>(); + auth.add(authLine); + reqHeaders.put(CLIENT_AUTH_HEADER, auth); + } } - else { - assertEquals(200, rc); - assertEquals("OK", bc.toString()); - return; - } - // the second access attempt should be sucessful - String credentials = user + ":" + pwd; - byte[] credentialsBytes = ByteChunk.convertToBytes(credentials); - String base64auth = Base64.encode(credentialsBytes); - String authScheme = verifyAuthSchemeCase ? "bAsIc " : "Basic "; - String authLine = authScheme + base64auth; + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); - 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); + if (expectedReject) { + assertEquals(expectedRC, rc); assertTrue(bc.getLength() > 0); } else { assertEquals(200, rc); assertEquals("OK", bc.toString()); + List newCookies = respHeaders.get(SERVER_COOKIE_HEADER); + if (newCookies != null) { + // harvest cookies whenever the server sends some new ones + cookies = newCookies; + } } } + /* + * 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(); // add the test user and role to the Realm tomcat.addUser(USER, PWD); 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 { + private void setUpNonLogin() 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); + ctxt.setSessionTimeout(LONG_TIMEOUT_MINS); - // Add protected servlet + // Add protected servlet to the context Tomcat.addServlet(ctxt, "TesterServlet1", new TesterServlet()); ctxt.addServletMapping(URI_PROTECTED, "TesterServlet1"); @@ -261,7 +416,7 @@ sc1.addCollection(collection1); ctxt.addConstraint(sc1); - // Add unprotected servlet + // Add unprotected servlet to the context Tomcat.addServlet(ctxt, "TesterServlet2", new TesterServlet()); ctxt.addServletMapping(URI_PUBLIC, "TesterServlet2"); @@ -276,17 +431,18 @@ LoginConfig lc = new LoginConfig(); lc.setAuthMethod("NONE"); ctxt.setLoginConfig(lc); - ctxt.getPipeline().addValve(new NonLoginAuthenticator()); + nonloginAuthenticator = new NonLoginAuthenticator(); + ctxt.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, System.getProperty("java.io.tmpdir")); - ctxt.setSessionTimeout(SHORT_TIMEOUT_SECS); + ctxt.setSessionTimeout(SHORT_TIMEOUT_MINS); - // Add protected servlet + // Add protected servlet to the context Tomcat.addServlet(ctxt, "TesterServlet3", new TesterServlet()); ctxt.addServletMapping(URI_PROTECTED, "TesterServlet3"); SecurityCollection collection = new SecurityCollection(); @@ -296,7 +452,7 @@ sc.addCollection(collection); ctxt.addConstraint(sc); - // Add unprotected servlet + // Add unprotected servlet to the context Tomcat.addServlet(ctxt, "TesterServlet4", new TesterServlet()); ctxt.addServletMapping(URI_PUBLIC, "TesterServlet4"); @@ -311,7 +467,17 @@ LoginConfig lc = new LoginConfig(); lc.setAuthMethod("BASIC"); ctxt.setLoginConfig(lc); - ctxt.getPipeline().addValve(new BasicAuthenticator()); + basicAuthenticator = new BasicAuthenticator(); + ctxt.getPipeline().addValve(basicAuthenticator); } + /* + * Force non-default behaviour for both Authenticators + */ + private void setAlwaysUseSession() { + + basicAuthenticator.setAlwaysUseSession(true); + nonloginAuthenticator.setAlwaysUseSession(true); + } + } \ No newline at end of file