| /* |
| * 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.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import jakarta.servlet.ServletException; |
| import jakarta.servlet.http.HttpServlet; |
| import jakarta.servlet.http.HttpServletRequest; |
| import jakarta.servlet.http.HttpServletResponse; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import org.apache.catalina.Context; |
| import org.apache.catalina.Manager; |
| import org.apache.catalina.Session; |
| import org.apache.catalina.connector.Request; |
| import org.apache.catalina.connector.Response; |
| import org.apache.catalina.startup.TesterServlet; |
| import org.apache.catalina.startup.Tomcat; |
| import org.apache.catalina.startup.TomcatBaseTest; |
| import org.apache.catalina.valves.ValveBase; |
| 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; |
| |
| public class TestSSOChangeSessionId extends TomcatBaseTest { |
| |
| protected static final boolean USE_COOKIES = true; |
| |
| private static final String NOTSORANDOMID = "NOTSORANNDOMID"; |
| 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 = "/test"; |
| private static final String URI_PROTECTED = "/protected"; |
| private static final String URI_AUTHENTICATION = "/authentication"; |
| |
| private Context testContext; |
| private SingleSignOn singleSignOn; |
| |
| private static final String SERVER_COOKIE_HEADER = "Set-Cookie"; |
| private static final String CLIENT_COOKIE_HEADER = "Cookie"; |
| |
| private List<String> cookies; |
| |
| /* |
| * 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 |
| final Tomcat tomcat = getTomcatInstance(); |
| |
| // Must have a real docBase for webapps - just use temp |
| testContext = tomcat.addContext(CONTEXT_PATH, System.getProperty("java.io.tmpdir")); |
| |
| singleSignOn = new SingleSignOn(); |
| tomcat.getHost().getPipeline().addValve(singleSignOn); |
| |
| testContext.getPipeline().addValve(new SessionManipulationFilter()); |
| |
| // Add protected servlet to the context |
| Tomcat.addServlet(testContext, "TesterServlet1", new TesterServlet()); |
| testContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1"); |
| |
| SecurityCollection collection1 = new SecurityCollection(); |
| collection1.addPatternDecoded(URI_PROTECTED); |
| SecurityConstraint sc1 = new SecurityConstraint(); |
| sc1.addAuthRole(ROLE); |
| sc1.addCollection(collection1); |
| testContext.addConstraint(sc1); |
| |
| // Add Authenticator |
| Tomcat.addServlet(testContext, "LoginServlet", new LoginServlet()); |
| testContext.addServletMappingDecoded(URI_AUTHENTICATION, "LoginServlet"); |
| |
| SecurityCollection collection2 = new SecurityCollection(); |
| collection2.addPatternDecoded(URI_AUTHENTICATION); |
| SecurityConstraint sc2 = new SecurityConstraint(); |
| // do not add a role - which signals access permitted without one |
| sc2.addCollection(collection2); |
| testContext.addConstraint(sc2); |
| |
| // Configure the authenticator and inherit the Realm from Engine |
| LoginConfig lc = new LoginConfig(); |
| lc.setAuthMethod("NONE"); |
| testContext.setLoginConfig(lc); |
| AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator(); |
| testContext.getPipeline().addValve(nonloginAuthenticator); |
| |
| // add the test user and role to the Realm |
| tomcat.addUser(USER, PWD); |
| tomcat.addRole(USER, ROLE); |
| |
| tomcat.start(); |
| } |
| |
| private static class SessionManipulationFilter extends ValveBase { |
| @Override |
| public void invoke(Request request, Response response) throws IOException, ServletException { |
| |
| if (request.getPrincipal() != null) { |
| final Manager manager = request.getContext().getManager(); |
| final Session session = manager.findSession(request.getSession().getId()); |
| manager.changeSessionId(session, NOTSORANDOMID); |
| } |
| |
| getNext().invoke(request, response); |
| } |
| } |
| |
| private static class LoginServlet extends HttpServlet { |
| |
| private static final long serialVersionUID = 1L; |
| |
| @Override |
| protected void doGet(HttpServletRequest request, HttpServletResponse response) |
| throws ServletException, IOException { |
| |
| if (request.getUserPrincipal() == null) { |
| request.login(USER, PWD); |
| |
| response.setContentType("text/plain"); |
| PrintWriter out = response.getWriter(); |
| out.print("OK"); |
| |
| } else { |
| response.setStatus(HttpServletResponse.SC_CONFLICT); |
| } |
| } |
| } |
| |
| @Test |
| public void testChangeSessionId() throws Exception { |
| final Map<String,SingleSignOnEntry> cache = singleSignOn.cache; |
| Assert.assertTrue("No SSO entries should be present at startup", cache.isEmpty()); |
| |
| // Authenticate a session |
| doTest(CONTEXT_PATH + URI_AUTHENTICATION, USE_COOKIES, HttpServletResponse.SC_OK); |
| |
| Assert.assertFalse("SSO must now be present", cache.isEmpty()); |
| |
| Assert.assertEquals("Only one SSO entry must be present", 1, cache.size()); |
| final Collection<SingleSignOnEntry> ssoEntries = cache.values(); |
| Assert.assertEquals("Only one session should be present, as there is only one context in the test", 1, |
| ssoEntries.size()); |
| |
| final SingleSignOnEntry singleSignOnEntry = ssoEntries.iterator().next(); |
| final Set<SingleSignOnSessionKey> sessions = singleSignOnEntry.findSessions(); |
| |
| Assert.assertEquals(1, sessions.size()); |
| |
| final SingleSignOnSessionKey singleSignOnSessionKey1 = sessions.iterator().next(); |
| Assert.assertNotEquals("A random SessionId is expected", NOTSORANDOMID, singleSignOnSessionKey1.getSessionId()); |
| |
| // Perform the request that changes the session id: |
| doTest(CONTEXT_PATH + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_OK); |
| |
| Assert.assertEquals("2 sessions means we have a zombie SingleSignOnSessionKey", 1, sessions.size()); |
| |
| final SingleSignOnSessionKey singleSignOnSessionKey2 = sessions.iterator().next(); |
| Assert.assertEquals(NOTSORANDOMID, singleSignOnSessionKey2.getSessionId()); |
| |
| final Session session = testContext.getManager().findSession(NOTSORANDOMID); |
| Assert.assertNotNull("We need a session in order to test expiry", session); |
| |
| session.expire(); |
| |
| Assert.assertTrue("No SSO entries should be present after expiration of the session", cache.isEmpty()); |
| |
| // Shouldn't this return 401? |
| doTest(CONTEXT_PATH + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN); |
| } |
| |
| @After |
| @Override |
| public void tearDown() throws Exception { |
| super.tearDown(); |
| } |
| |
| public void doTest(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()); |
| saveCookies(respHeaders); |
| } |
| } |
| |
| /* |
| * 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); |
| } |
| } |
| |
| /* |
| * 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(';'))); |
| } |
| } |
| } |
| } |