This is an automated email from the ASF dual-hosted git repository. dakirily pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git
commit 23003b1db8b591ad686501b5df151f64246db34b Author: Daniil Kirilyuk <[email protected]> AuthorDate: Sat Sep 19 19:48:34 2026 +0200 NO-JIRA: [Broker-J] Renew HTTP management sessions after authentication --- .../server/management/plugin/HttpManagement.java | 2 + .../management/plugin/HttpManagementUtil.java | 27 +- .../management/plugin/HttpManagementUtilTest.java | 114 ++++++- .../auth/InteractiveAuthenticationSessionTest.java | 380 +++++++++++++++++++++ .../auth/OAuth2InteractiveAuthenticatorTest.java | 7 + .../PreemptiveAuthenticationTest.java | 110 +++++- .../qpid/tests/http/authentication/SaslTest.java | 61 ++-- .../authentication/SpnegoAuthenticationTest.java | 235 +++++++++++++ 8 files changed, 894 insertions(+), 42 deletions(-) diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java index 07b0e4e550..3eb3b1256e 100644 --- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java +++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java @@ -46,6 +46,7 @@ import javax.net.ssl.SSLSessionContext; import jakarta.servlet.DispatcherType; import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.SessionTrackingMode; import jakarta.servlet.http.HttpServletRequest; import org.eclipse.jetty.ee11.servlet.ErrorPageErrorHandler; @@ -357,6 +358,7 @@ public class HttpManagement extends AbstractPluginAdapter<HttpManagement> implem corsHandler.setAllowCredentials(getCorsAllowCredentials()); final ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + root.getSessionHandler().setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE)); root.insertHandler(rewriteHandler); root.insertHandler(corsHandler); server.setHandler(root); diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagementUtil.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagementUtil.java index dbe02091b9..9e60fb4d91 100644 --- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagementUtil.java +++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagementUtil.java @@ -152,18 +152,25 @@ public class HttpManagementUtil SubjectExecutionContext.withSubject(subject, () -> broker.authorise(MANAGE_ACTION)); } - public static void saveAuthorisedSubject(HttpServletRequest request, Subject subject) + public static void saveAuthorisedSubject(final HttpServletRequest request, final Subject original) { - HttpSession session = request.getSession(); - Broker<?> broker = getBroker(session.getServletContext()); - HttpPort<?> port = HttpManagementUtil.getPort(request); + final HttpSession session = request.getSession(); + try + { + request.changeSessionId(); + } + catch (final IllegalStateException e) + { + throw new SessionInvalidatedException(); + } + + final Subject subject = createServletConnectionSubject(request, original); + final Broker<?> broker = getBroker(session.getServletContext()); + final HttpPort<?> port = HttpManagementUtil.getPort(request); setSessionAttribute(ATTR_SUBJECT, subject, session, request); - setSessionAttribute(ATTR_LOGIN_LOGOUT_REPORTER, - new LoginLogoutReporter(subject, broker), - session, - request); + setSessionAttribute(ATTR_LOGIN_LOGOUT_REPORTER, new LoginLogoutReporter(subject, broker), session, request); - long absoluteSessionTimeout = port.getAbsoluteSessionTimeout(); + final long absoluteSessionTimeout = port.getAbsoluteSessionTimeout(); if (absoluteSessionTimeout > 0) { scheduleAbsoluteSessionTimeout(request, session, broker, absoluteSessionTimeout); @@ -358,6 +365,6 @@ public class HttpManagementUtil { final Subject subject = createServletConnectionSubject(request, original); assertManagementAccess(broker, subject); - saveAuthorisedSubject(request, subject); + saveAuthorisedSubject(request, original); } } diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/HttpManagementUtilTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/HttpManagementUtilTest.java index 719de54ffe..e3af7216e1 100644 --- a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/HttpManagementUtilTest.java +++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/HttpManagementUtilTest.java @@ -22,24 +22,82 @@ package org.apache.qpid.server.management.plugin; import static org.junit.jupiter.api.Assertions.assertEquals; - +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import javax.security.auth.Subject; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.apache.qpid.server.management.plugin.servlet.ServletConnectionPrincipal; import org.apache.qpid.server.model.Broker; +import org.apache.qpid.server.model.port.HttpPort; +import org.apache.qpid.server.security.AccessDeniedException; import org.apache.qpid.server.security.SubjectExecutionContext; import org.apache.qpid.server.security.access.Operation; +import org.apache.qpid.server.security.auth.AuthenticatedPrincipal; import org.apache.qpid.test.utils.UnitTestBase; public class HttpManagementUtilTest extends UnitTestBase { + private HttpServletRequest _request; + private HttpSession _session; + private Broker<?> _broker; + private Map<String, Object> _attributes; + + @BeforeEach + public void setUp() + { + _request = mock(HttpServletRequest.class); + _session = mock(HttpSession.class); + _broker = mock(Broker.class); + _attributes = new HashMap<>(); + final HttpPort<?> port = mock(HttpPort.class); + final ServletContext context = mock(ServletContext.class); + final AtomicReference<String> sessionId = new AtomicReference<>("initial-session"); + when(_request.getAttribute("org.apache.qpid.server.model.Port")).thenReturn(port); + when(port.getId()).thenReturn(UUID.randomUUID()); + when(_request.getSession()).thenReturn(_session); + when(_request.getSession(false)).thenReturn(_session); + when(_request.getRemoteHost()).thenReturn("localhost"); + when(_session.getServletContext()).thenReturn(context); + when(context.getAttribute(HttpManagementUtil.ATTR_BROKER)).thenReturn(_broker); + when(_session.getId()).thenAnswer(invocation -> sessionId.get()); + when(_request.changeSessionId()).thenAnswer(invocation -> + { + sessionId.set("renewed-session"); + return sessionId.get(); + }); + when(_session.getAttribute(anyString())).thenAnswer(invocation -> _attributes.get(invocation.getArgument(0))); + doAnswer(invocation -> + { + _attributes.put(invocation.getArgument(0), invocation.getArgument(1)); + return null; + }).when(_session).setAttribute(anyString(), any()); + } + @Test public void testEnsureFilenameIsRfc2183() { @@ -50,18 +108,64 @@ public class HttpManagementUtilTest extends UnitTestBase @Test public void testAssertManagementAccessUsesSubject() { - final Broker<?> broker = mock(Broker.class); final Subject subject = new Subject(); final AtomicReference<Subject> capturedSubject = new AtomicReference<>(); - doAnswer(invocation -> { capturedSubject.set(SubjectExecutionContext.currentSubject()); return null; - }).when(broker).authorise(any(Operation.class)); + }).when(_broker).authorise(any(Operation.class)); - HttpManagementUtil.assertManagementAccess(broker, subject); + HttpManagementUtil.assertManagementAccess(_broker, subject); assertEquals(subject, capturedSubject.get(), "Unexpected subject"); } + + @Test + public void testAuthenticationRenewsSessionBeforePublishingSubject() + { + final AuthenticatedPrincipal principal = new AuthenticatedPrincipal(() -> "user"); + final Subject original = new Subject(true, Set.of(principal), Set.of(), Set.of()); + _attributes.put("negotiationState", "retained"); + + HttpManagementUtil.createServletConnectionSubjectAssertManagementAccessAndSave(_broker, _request, original); + + final InOrder order = inOrder(_broker, _request, _session); + order.verify(_broker).authorise(any(Operation.class)); + order.verify(_request).changeSessionId(); + order.verify(_session).setAttribute(eq(HttpManagementUtil + .getRequestSpecificAttributeName("Qpid.subject", _request)), any(Subject.class)); + final Subject saved = HttpManagementUtil.getAuthorisedSubject(_request); + assertNotNull(saved); + assertTrue(saved.isReadOnly()); + assertEquals(Set.of(principal), saved.getPrincipals(AuthenticatedPrincipal.class)); + assertEquals("retained", _attributes.get("negotiationState")); + assertEquals(1, saved.getPrincipals(ServletConnectionPrincipal.class).size()); + assertEquals(new ServletConnectionPrincipal(_request).getSessionId(), + saved.getPrincipals(ServletConnectionPrincipal.class).iterator().next().getSessionId()); + assertEquals(Set.of(principal), original.getPrincipals()); + } + + @Test + public void testFailedRenewalDoesNotPublishSubject() + { + when(_request.changeSessionId()).thenThrow(new IllegalStateException("Session expired")); + + assertThrows(SessionInvalidatedException.class, () -> + HttpManagementUtil.saveAuthorisedSubject(_request, new Subject())); + + verify(_session, never()).setAttribute(anyString(), any()); + } + + @Test + public void testDeniedManagementAccessDoesNotRenewSession() + { + doThrow(new AccessDeniedException("Denied")).when(_broker).authorise(any(Operation.class)); + + assertThrows(AccessDeniedException.class, () -> HttpManagementUtil + .createServletConnectionSubjectAssertManagementAccessAndSave(_broker, _request, new Subject())); + + verify(_request, never()).changeSessionId(); + verify(_session, never()).setAttribute(anyString(), any()); + } } diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/InteractiveAuthenticationSessionTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/InteractiveAuthenticationSessionTest.java new file mode 100644 index 0000000000..39278f9f20 --- /dev/null +++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/InteractiveAuthenticationSessionTest.java @@ -0,0 +1,380 @@ +/* + * + * 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.qpid.server.management.plugin.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.Serial; +import java.security.cert.X509Certificate; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; + +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionBindingEvent; +import jakarta.servlet.http.HttpSessionBindingListener; +import jakarta.servlet.http.HttpSessionIdListener; +import org.eclipse.jetty.ee11.servlet.ServletContextHandler; +import org.eclipse.jetty.ee11.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import org.apache.qpid.server.logging.EventLogger; +import org.apache.qpid.server.management.plugin.HttpManagementConfiguration; +import org.apache.qpid.server.management.plugin.HttpManagementUtil; +import org.apache.qpid.server.management.plugin.HttpRequestInteractiveAuthenticator; +import org.apache.qpid.server.management.plugin.servlet.ServletConnectionPrincipal; +import org.apache.qpid.server.model.Broker; +import org.apache.qpid.server.model.port.HttpPort; +import org.apache.qpid.server.security.SubjectCreator; +import org.apache.qpid.server.security.auth.AuthenticatedPrincipal; +import org.apache.qpid.server.security.auth.AuthenticationResult; +import org.apache.qpid.server.security.auth.SubjectAuthenticationResult; +import org.apache.qpid.server.security.auth.manager.ExternalAuthenticationManager; +import org.apache.qpid.server.security.auth.manager.KerberosAuthenticationManager; +import org.apache.qpid.test.utils.UnitTestBase; + +public class InteractiveAuthenticationSessionTest extends UnitTestBase +{ + private final AtomicReference<HttpSession> _session = new AtomicReference<>(); + private final AtomicReference<Runnable> _expiry = new AtomicReference<>(); + private final AtomicInteger _renewals = new AtomicInteger(); + private final AtomicInteger _unbindings = new AtomicInteger(); + private Server _server; + private LocalConnector _connector; + private Broker<?> _broker; + private HttpPort<?> _port; + private HttpManagementConfiguration<?> _configuration; + private HttpRequestInteractiveAuthenticator _authenticator; + private boolean _certificateAvailable; + private ScheduledFuture<?> _expiryFuture; + + @BeforeEach + public void setUp() throws Exception + { + _session.set(null); + _expiry.set(null); + _renewals.set(0); + _unbindings.set(0); + _certificateAvailable = false; + _broker = mock(Broker.class); + _port = mock(HttpPort.class); + _configuration = mock(HttpManagementConfiguration.class); + _expiryFuture = mock(ScheduledFuture.class); + when(_broker.getEventLogger()).thenReturn(mock(EventLogger.class)); + when(_port.getId()).thenReturn(UUID.randomUUID()); + doReturn(_port).when(_configuration).getPort(any()); + doAnswer(invocation -> + { + _expiry.set(invocation.getArgument(2)); + return _expiryFuture; + }).when(_broker).scheduleTask(eq(60000L), eq(TimeUnit.MILLISECONDS), any(Runnable.class)); + + _server = new Server(); + _connector = new LocalConnector(_server); + _server.addConnector(_connector); + final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + context.getSessionHandler().setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE)); + context.getServletContext().setAttribute(HttpManagementUtil.ATTR_BROKER, _broker); + context.addEventListener((HttpSessionIdListener) (event, oldId) -> _renewals.incrementAndGet()); + context.addServlet(new ServletHolder(new SessionServlet()), "/*"); + _server.setHandler(context); + _server.start(); + } + + @AfterEach + public void tearDown() throws Exception + { + if (_server != null) + { + _server.stop(); + } + } + + @ParameterizedTest + @EnumSource(Mechanism.class) + public void testAuthenticationRenewsExistingSession(final Mechanism mechanism) throws Exception + { + configure(mechanism, true); + final String previousCookie = cookie(request("/prepare", null)); + final HttpSession session = _session.get(); + final String previousId = session.getId(); + final long creationTime = session.getCreationTime(); + session.setMaxInactiveInterval(60); + final HttpTester.Response response = request("/login", previousCookie); + final String renewedCookie = cookie(response); + + assertNotEquals(previousCookie, renewedCookie); + assertNotEquals(previousId, session.getId()); + assertEquals(creationTime, session.getCreationTime()); + assertEquals(60, session.getMaxInactiveInterval()); + assertEquals(1, _renewals.get()); + assertNotNull(session.getAttribute("loginState")); + assertEquals(0, _unbindings.get()); + assertAuditSessionId(session); + assertEquals("authenticated", request("/state", renewedCookie).getContent()); + assertEquals("anonymous", request("/state", previousCookie).getContent()); + + final CompletableFuture<?>[] requests = new CompletableFuture<?>[3]; + for (int i = 0; i < requests.length; i++) + { + requests[i] = CompletableFuture.runAsync(() -> + { + try + { + assertEquals("authenticated", request("/state", renewedCookie).getContent()); + } + catch (Exception e) + { + throw new CompletionException(e); + } + }); + } + CompletableFuture.allOf(requests).get(10, TimeUnit.SECONDS); + assertEquals(1, _renewals.get()); + } + + @ParameterizedTest + @EnumSource(Mechanism.class) + public void testAuthenticationCreatesSession(final Mechanism mechanism) throws Exception + { + configure(mechanism, true); + final HttpTester.Response response = request("/login", null); + + assertNotNull(cookie(response)); + assertNotNull(_session.get()); + assertAuditSessionId(_session.get()); + assertEquals(1, _renewals.get()); + assertEquals("authenticated", request("/state", cookie(response)).getContent()); + } + + @ParameterizedTest + @EnumSource(Mechanism.class) + public void testFailedAuthenticationDoesNotRenewSession(final Mechanism mechanism) throws Exception + { + configure(mechanism, false); + final String cookie = cookie(request("/prepare", null)); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, request("/login", cookie).getStatus()); + assertEquals(0, _renewals.get()); + assertEquals("anonymous", request("/state", cookie).getContent()); + } + + @ParameterizedTest + @EnumSource(Mechanism.class) + public void testAbsoluteTimeoutInvalidatesRenewedSession(final Mechanism mechanism) throws Exception + { + configure(mechanism, true); + when(_port.getAbsoluteSessionTimeout()).thenReturn(60000L); + final String cookie = cookie(request("/prepare", null)); + final String renewedCookie = cookie(request("/login", cookie)); + assertNotNull(_expiry.get()); + assertEquals(0, _unbindings.get()); + + _expiry.get().run(); + + assertEquals(1, _unbindings.get()); + assertEquals("anonymous", request("/state", renewedCookie).getContent()); + verify(_expiryFuture).cancel(false); + } + + @ParameterizedTest + @EnumSource(Mechanism.class) + public void testLogoutInvalidatesRenewedSession(final Mechanism mechanism) throws Exception + { + configure(mechanism, true); + final String cookie = cookie(request("/prepare", null)); + final String renewedCookie = cookie(request("/login", cookie)); + + request("/logout", renewedCookie); + + assertEquals(1, _unbindings.get()); + assertEquals("anonymous", request("/state", renewedCookie).getContent()); + } + + @Test + public void testCookieTrackingDoesNotEncodeSessionIdentifiers() throws Exception + { + assertEquals("/resource\n/resource", request("/encode", null).getContent()); + } + + private void configure(final Mechanism mechanism, final boolean successful) + { + final SubjectCreator creator = mock(SubjectCreator.class); + final Subject subject = new Subject(true, Set.of(new AuthenticatedPrincipal(() -> "user")), Set.of(), Set.of()); + when(_port.getSubjectCreator(anyBoolean(), anyString())).thenReturn(creator); + if (mechanism == Mechanism.SPNEGO) + { + final KerberosAuthenticationManager provider = mock(KerberosAuthenticationManager.class); + doReturn(provider).when(_configuration).getAuthenticationProvider(any()); + doReturn(_broker).when(provider).getParent(); + final AuthenticationResult result = mock(AuthenticationResult.class); + when(result.getStatus()).thenReturn(successful ? AuthenticationResult.AuthenticationStatus.SUCCESS : + AuthenticationResult.AuthenticationStatus.ERROR); + when(provider.authenticate(any())).thenReturn(result); + final SubjectAuthenticationResult authentication = mock(SubjectAuthenticationResult.class); + when(authentication.getSubject()).thenReturn(subject); + when(creator.createResultWithGroups(result)).thenReturn(authentication); + _authenticator = new SpnegoInteractiveAuthenticator(); + } + else + { + final ExternalAuthenticationManager<?> provider = mock(ExternalAuthenticationManager.class); + doReturn(provider).when(_configuration).getAuthenticationProvider(any()); + doReturn(_broker).when(provider).getParent(); + when(creator.createSubjectWithGroups(any(AuthenticatedPrincipal.class))).thenReturn(subject); + _certificateAvailable = successful; + _authenticator = new SSLClientCertInteractiveAuthenticator(); + } + } + + private void assertAuditSessionId(final HttpSession session) + { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRemoteHost()).thenReturn("localhost"); + when(request.getSession(false)).thenReturn(session); + final Subject subject = (Subject) session.getAttribute("Qpid.subject." + _port.getId()); + assertNotNull(subject); + assertEquals(new ServletConnectionPrincipal(request).getSessionId(), + subject.getPrincipals(ServletConnectionPrincipal.class).iterator().next().getSessionId()); + } + + private HttpTester.Response request(final String path, final String cookie) throws Exception + { + return HttpTester.parseResponse(_connector.getResponse("GET " + path + " HTTP/1.1\r\nHost: localhost\r\n" + + (cookie == null ? "" : "Cookie: " + cookie + "\r\n") + "Connection: close\r\n\r\n")); + } + + private String cookie(final HttpTester.Response response) + { + final String cookie = response.get("Set-Cookie"); + assertNotNull(cookie, "Expected a session cookie"); + return cookie.split(";", 2)[0]; + } + + private class SessionServlet extends HttpServlet + { + @Serial + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) + throws IOException, ServletException + { + request.setAttribute("org.apache.qpid.server.model.Port", _port); + final String path = request.getPathInfo(); + if ("/prepare".equals(path)) + { + final HttpSession session = request.getSession(); + session.setAttribute("loginState", new HttpSessionBindingListener() + { + @Override + public void valueUnbound(final HttpSessionBindingEvent event) + { + _unbindings.incrementAndGet(); + } + }); + _session.set(session); + } + else if ("/login".equals(path)) + { + if (_certificateAvailable) + { + final X509Certificate certificate = mock(X509Certificate.class); + when(certificate.getSubjectX500Principal()).thenReturn(new X500Principal("CN=user")); + request.setAttribute("jakarta.servlet.request.X509Certificate", new X509Certificate[]{certificate}); + } + final HttpServletRequest secureRequest = new HttpServletRequestWrapper(request) + { + @Override + public boolean isSecure() + { + return true; + } + + @Override + public HttpSession getSession() + { + final HttpSession session = super.getSession(); + _session.set(session); + return session; + } + }; + _authenticator.getAuthenticationHandler(secureRequest, _configuration).handleAuthentication(response); + } + else if ("/encode".equals(path)) + { + request.getSession(); + response.getWriter().write(response.encodeURL("/resource") + "\n" + + response.encodeRedirectURL("/resource")); + } + else if ("/logout".equals(path)) + { + request.getSession(false).invalidate(); + } + else + { + response.getWriter().write(HttpManagementUtil.getAuthorisedSubject(request) == null ? + "anonymous" : "authenticated"); + } + } + } + + private enum Mechanism + { + SPNEGO, + CLIENT_CERTIFICATE + } +} diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/OAuth2InteractiveAuthenticatorTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/OAuth2InteractiveAuthenticatorTest.java index 9058166e34..e681a16df2 100644 --- a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/OAuth2InteractiveAuthenticatorTest.java +++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/auth/OAuth2InteractiveAuthenticatorTest.java @@ -29,7 +29,9 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -57,6 +59,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.apache.qpid.server.management.plugin.HttpManagementConfiguration; import org.apache.qpid.server.management.plugin.HttpManagementUtil; @@ -168,6 +171,9 @@ public class OAuth2InteractiveAuthenticatorTest extends UnitTestBase ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class); verify(mockResponse).sendRedirect(argument.capture()); + final InOrder order = inOrder(mockRequest, mockResponse); + order.verify(mockRequest).changeSessionId(); + order.verify(mockResponse).sendRedirect(TEST_REQUEST); assertEquals(TEST_REQUEST, argument.getValue(), "Wrong redirect"); String attrSubject = HttpManagementUtil.getRequestSpecificAttributeName(ATTR_SUBJECT, mockRequest); assertNotNull(sessionAttributes.get(attrSubject), "No subject on session"); @@ -284,6 +290,7 @@ public class OAuth2InteractiveAuthenticatorTest extends UnitTestBase HttpServletResponse mockResponse = mock(HttpServletResponse.class); authenticationHandler.handleAuthentication(mockResponse); verify(mockResponse).sendError(eq(403), any(String.class)); + verify(mockRequest, never()).changeSessionId(); } private Map<String, String> getRedirectParameters(final String redirectLocation) diff --git a/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/PreemptiveAuthenticationTest.java b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/PreemptiveAuthenticationTest.java index 8fef21364a..611437f09b 100644 --- a/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/PreemptiveAuthenticationTest.java +++ b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/PreemptiveAuthenticationTest.java @@ -23,15 +23,21 @@ package org.apache.qpid.tests.http.authentication; import static jakarta.servlet.http.HttpServletResponse.SC_CREATED; import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.InetAddress; import java.security.KeyStore; @@ -133,6 +139,76 @@ public class PreemptiveAuthenticationTest extends HttpTestBase assertThat(status, is(equalTo(HttpURLConnection.HTTP_OK))); } + @Test + public void clientAuthenticationRenewsSession() throws Exception + { + final HttpTestHelper helper = configForClientAuth("CN=localhost"); + helper.setUserName(null); + final String initialCookie = prepareSession(helper); + + final HttpURLConnection login = helper.openManagementConnection(HttpManagement.DEFAULT_LOGIN_URL, "GET"); + final String renewedCookie; + try + { + login.setInstanceFollowRedirects(false); + login.setRequestProperty("Cookie", initialCookie); + assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, login.getResponseCode()); + final String header = login.getHeaderField("Set-Cookie"); + assertNotNull(header); + final HttpCookie cookie = HttpCookie.parse(header).get(0); + assertTrue(cookie.getSecure(), "HTTPS session cookies must remain secure"); + assertTrue(cookie.isHttpOnly(), "Session cookies must remain HttpOnly"); + renewedCookie = cookie.getName() + "=" + cookie.getValue(); + } + finally + { + login.disconnect(); + } + + assertNotEquals(initialCookie, renewedCookie, "Authentication must renew the session cookie"); + assertEquals("localhost", getSessionUser(helper, renewedCookie)); + assertNull(getSessionUser(helper, initialCookie)); + + final HttpURLConnection logout = helper.openManagementConnection("/logout", "GET"); + try + { + logout.setInstanceFollowRedirects(false); + logout.setRequestProperty("Cookie", renewedCookie); + assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, logout.getResponseCode()); + } + finally + { + logout.disconnect(); + } + assertNull(getSessionUser(helper, renewedCookie)); + } + + @Test + public void anonymousAuthenticationRenewsSession() throws Exception + { + final HttpTestHelper helper = configForAnonymous(); + helper.setUserName(null); + final String initialCookie = prepareSession(helper); + final HttpURLConnection login = helper.openManagementConnection(HttpManagement.DEFAULT_LOGIN_URL, "GET"); + final String renewedCookie; + try + { + login.setInstanceFollowRedirects(false); + login.setRequestProperty("Cookie", initialCookie); + assertEquals(SC_OK, login.getResponseCode()); + final String cookie = login.getHeaderField("Set-Cookie"); + assertNotNull(cookie); + renewedCookie = cookie.split(";", 2)[0]; + } + finally + { + login.disconnect(); + } + assertNotEquals(initialCookie, renewedCookie); + assertEquals("ANONYMOUS", getSessionUser(helper, renewedCookie)); + assertNull(getSessionUser(helper, initialCookie)); + } + @Test public void clientAuthUnrecognisedCert() throws Exception { @@ -195,6 +271,38 @@ public class PreemptiveAuthenticationTest extends HttpTestBase assertThat("Unexpected cookie", conn.getHeaderFields(), not(hasKey("Set-Cookie"))); } + private String prepareSession(final HttpTestHelper helper) throws IOException + { + final HttpURLConnection connection = helper.openManagementConnection("/service/sasl", "GET"); + try + { + assertEquals(SC_OK, connection.getResponseCode()); + assertNull(helper.readJsonResponseAsMap(connection).get("user")); + final String cookie = connection.getHeaderField("Set-Cookie"); + assertNotNull(cookie); + return cookie.split(";", 2)[0]; + } + finally + { + connection.disconnect(); + } + } + + private Object getSessionUser(final HttpTestHelper helper, final String cookie) throws IOException + { + final HttpURLConnection connection = helper.openManagementConnection("/service/sasl", "GET"); + try + { + connection.setRequestProperty("Cookie", cookie); + assertEquals(SC_OK, connection.getResponseCode()); + return helper.readJsonResponseAsMap(connection).get("user"); + } + finally + { + connection.disconnect(); + } + } + private void verifyGetBroker(int expectedResponseCode) throws Exception { assertThat(getHelper().submitRequest("broker", "GET"), is(equalTo(expectedResponseCode))); diff --git a/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SaslTest.java b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SaslTest.java index a77fc10ac7..59ee926edf 100644 --- a/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SaslTest.java +++ b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SaslTest.java @@ -32,7 +32,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.OutputStream; +import java.net.CookieManager; +import java.net.CookiePolicy; import java.net.HttpURLConnection; +import java.net.URI; import java.util.Base64; import java.util.List; import java.util.Map; @@ -104,9 +107,10 @@ public class SaslTest extends HttpTestBase try { assertEquals(SC_OK, connection.getResponseCode(), "Unexpected response"); - handleChallengeAndSendResponse(connection, _userName, _userPassword, PlainNegotiator.MECHANISM, SC_OK); + final List<String> cookies = handleChallengeAndSendResponse(connection, _userName, _userPassword, + PlainNegotiator.MECHANISM, SC_OK); - assertAuthenticatedUser(_userName, connection.getHeaderFields().get(SET_COOKIE_HEADER)); + assertAuthenticatedUser(_userName, cookies); } finally { @@ -265,8 +269,8 @@ public class SaslTest extends HttpTestBase } private List<String> plainSASLAuthenticationWithInitialResponse(final String userName, - final String userPassword, - final int expectedResponseCode) throws Exception + final String userPassword, + final int expectedResponseCode) throws Exception { byte[] responseBytes = generatePlainClientResponse(userName, userPassword); String responseData = Base64.getEncoder().encodeToString(responseBytes); @@ -299,8 +303,7 @@ public class SaslTest extends HttpTestBase HttpURLConnection connection = requestSASLAuthentication(mechanism); try { - handleChallengeAndSendResponse(connection, userName, userPassword, mechanism, expectedResponseCode); - return connection.getHeaderFields().get(SET_COOKIE_HEADER); + return handleChallengeAndSendResponse(connection, userName, userPassword, mechanism, expectedResponseCode); } finally { @@ -309,41 +312,47 @@ public class SaslTest extends HttpTestBase } - private void handleChallengeAndSendResponse(HttpURLConnection requestChallengeConnection, - String userName, - String userPassword, - String mechanism, - final int expectedResponseCode) + private List<String> handleChallengeAndSendResponse(final HttpURLConnection requestChallengeConnection, + final String userName, + final String userPassword, + final String mechanism, + final int expectedResponseCode) throws Exception { - Map<String, Object> response = getHelper().readJsonResponseAsMap(requestChallengeConnection); - String challenge = (String) response.get("challenge"); + final Map<String, Object> response = getHelper().readJsonResponseAsMap(requestChallengeConnection); + final String challenge = (String) response.get("challenge"); assertNotNull(challenge, "Challenge is not found"); - byte[] challengeBytes = Base64.getDecoder().decode(challenge); - byte[] responseBytes = generateClientResponse(mechanism, userName, userPassword, challengeBytes); - String responseData = Base64.getEncoder().encodeToString(responseBytes); - String requestParameters = (String.format("id=%s&response=%s", response.get("id"), responseData)); + final byte[] challengeBytes = Base64.getDecoder().decode(challenge); + final byte[] responseBytes = generateClientResponse(mechanism, userName, userPassword, challengeBytes); + final String responseData = Base64.getEncoder().encodeToString(responseBytes); + final String requestParameters = (String.format("id=%s&response=%s", response.get("id"), responseData)); - postResponse(requestChallengeConnection.getHeaderFields().get(SET_COOKIE_HEADER), - requestParameters, - expectedResponseCode); + return postResponse(requestChallengeConnection.getHeaderFields().get(SET_COOKIE_HEADER), + requestParameters, expectedResponseCode); } - private void postResponse(final List<String> cookies, - final String requestParameters, - final int expectedResponseCode) throws IOException + private List<String> postResponse(final List<String> cookies, + final String requestParameters, + final int expectedResponseCode) throws IOException { - HttpURLConnection authenticateConnection = getHelper().openManagementConnection(SASL_SERVICE, "POST"); + final HttpURLConnection authenticateConnection = getHelper().openManagementConnection(SASL_SERVICE, "POST"); try { applyCookiesToConnection(cookies, authenticateConnection); - try (OutputStream os = authenticateConnection.getOutputStream()) + try (final OutputStream os = authenticateConnection.getOutputStream()) { os.write(requestParameters.getBytes()); os.flush(); - assertEquals(expectedResponseCode, authenticateConnection.getResponseCode(), "Unexpected response code"); + assertEquals(expectedResponseCode, authenticateConnection.getResponseCode(), + "Unexpected response code"); } + final CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL); + final URI uri = URI.create(authenticateConnection.getURL().toExternalForm()); + cookieManager.put(uri, Map.of(SET_COOKIE_HEADER, cookies)); + cookieManager.put(uri, authenticateConnection.getHeaderFields()); + return cookieManager.getCookieStore().getCookies().stream() + .map(cookie -> cookie.getName() + "=" + cookie.getValue()).toList(); } finally { diff --git a/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SpnegoAuthenticationTest.java b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SpnegoAuthenticationTest.java new file mode 100644 index 0000000000..d7a714cd08 --- /dev/null +++ b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/authentication/SpnegoAuthenticationTest.java @@ -0,0 +1,235 @@ +/* + * + * 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.qpid.tests.http.authentication; + +import static jakarta.servlet.http.HttpServletResponse.SC_CREATED; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; +import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.apache.qpid.server.test.KerberosUtilities.ACCEPT_SCOPE; +import static org.apache.qpid.server.test.KerberosUtilities.CLIENT_PRINCIPAL_FULL_NAME; +import static org.apache.qpid.server.test.KerberosUtilities.CLIENT_PRINCIPAL_NAME; +import static org.apache.qpid.server.test.KerberosUtilities.HOST_NAME; +import static org.apache.qpid.server.test.KerberosUtilities.REALM; +import static org.apache.qpid.server.test.KerberosUtilities.SERVICE_PRINCIPAL_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.File; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.util.ArrayDeque; +import java.util.Base64; +import java.util.Deque; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.apache.qpid.server.management.plugin.HttpManagement; +import org.apache.qpid.server.model.AuthenticationProvider; +import org.apache.qpid.server.model.ConfiguredObject; +import org.apache.qpid.server.model.Port; +import org.apache.qpid.server.model.Protocol; +import org.apache.qpid.server.model.Transport; +import org.apache.qpid.server.security.auth.manager.KerberosAuthenticationManager; +import org.apache.qpid.server.security.auth.manager.SpnegoAuthenticator; +import org.apache.qpid.server.test.KerberosUtilities; +import org.apache.qpid.test.utils.EmbeddedKdcExtension; +import org.apache.qpid.test.utils.SystemPropertySetter; +import org.apache.qpid.tests.http.HttpTestBase; +import org.apache.qpid.tests.http.HttpTestHelper; + +public class SpnegoAuthenticationTest extends HttpTestBase +{ + private static final String SASL_SERVICE = "/service/sasl"; + private static final KerberosUtilities UTILS = new KerberosUtilities(); + + @RegisterExtension + public static final EmbeddedKdcExtension KDC = new EmbeddedKdcExtension(HOST_NAME, 0, "QpidHttpTestKerberosServer", + REALM); + + @RegisterExtension + public static final SystemPropertySetter SYSTEM_PROPERTY_SETTER = new SystemPropertySetter(); + + private static File _clientKeyTabFile; + + private final Deque<String> _createdObjects = new ArrayDeque<>(); + private HttpTestHelper _kerberosHelper; + + @BeforeAll + public static void configureKerberos() throws Exception + { + UTILS.prepareConfiguration(HOST_NAME, SYSTEM_PROPERTY_SETTER); + _clientKeyTabFile = UTILS.prepareKeyTabs(KDC); + } + + @BeforeEach + public void configureHttpPort() throws Exception + { + final String provider = getTestName() + "-kerberos"; + final String port = getTestName() + "-http"; + final Map<String, String> context = Map.of("qpid.auth.gssapi.spnegoConfigScope", ACCEPT_SCOPE); + getHelper().submitRequest("authenticationprovider/" + provider, "PUT", + Map.of(AuthenticationProvider.TYPE, KerberosAuthenticationManager.PROVIDER_TYPE, + ConfiguredObject.CONTEXT, context), SC_CREATED); + _createdObjects.addFirst("authenticationprovider/" + provider); + getHelper().submitRequest("port/" + port, "PUT", + Map.of(Port.TYPE, "HTTP", Port.PORT, 0, Port.AUTHENTICATION_PROVIDER, provider, + Port.PROTOCOLS, Set.of(Protocol.HTTP), Port.TRANSPORTS, Set.of(Transport.TCP)), SC_CREATED); + _createdObjects.addFirst("port/" + port); + final int boundPort = ((Number) getHelper().getJsonAsMap("port/" + port).get("boundPort")).intValue(); + _kerberosHelper = new HttpTestHelper(getBrokerAdmin(), null, boundPort); + _kerberosHelper.setUserName(null); + } + + @AfterEach + public void removeHttpPort() throws Exception + { + while (!_createdObjects.isEmpty()) + { + getHelper().submitRequest(_createdObjects.removeFirst(), "DELETE", SC_OK); + } + } + + @Test + public void testInteractiveAuthenticationRenewsSession() throws Exception + { + final String initialCookie = prepareSession(); + final String renewedCookie = authenticate(initialCookie); + + assertNotEquals(initialCookie, renewedCookie, "Authentication must renew the session cookie"); + assertEquals(CLIENT_PRINCIPAL_FULL_NAME, getSessionUser(renewedCookie)); + assertNull(getSessionUser(initialCookie)); + } + + @Test + public void testInteractiveAuthenticationCreatesSession() throws Exception + { + assertEquals(CLIENT_PRINCIPAL_FULL_NAME, getSessionUser(authenticate(null))); + } + + @Test + public void testChallengeDoesNotAuthenticateSession() throws Exception + { + final String cookie = prepareSession(); + final HttpURLConnection connection = _kerberosHelper + .openManagementConnection(HttpManagement.DEFAULT_LOGIN_URL, "GET"); + try + { + connection.setRequestProperty("Cookie", cookie); + assertEquals(SC_UNAUTHORIZED, connection.getResponseCode()); + assertEquals(SpnegoAuthenticator.RESPONSE_AUTH_HEADER_VALUE_NEGOTIATE, + connection.getHeaderField(SpnegoAuthenticator.RESPONSE_AUTH_HEADER_NAME)); + } + finally + { + connection.disconnect(); + } + assertNull(getSessionUser(cookie)); + } + + @Test + public void testManagementSessionsUseCookieTracking() throws Exception + { + final String cookie = authenticate(prepareSession()); + final String sessionId = HttpCookie.parse(cookie).get(0).getValue(); + final HttpURLConnection connection = _kerberosHelper + .openManagementConnection(SASL_SERVICE + ";jsessionid=" + sessionId, "GET"); + try + { + assertEquals(SC_OK, connection.getResponseCode()); + assertNull(_kerberosHelper.readJsonResponseAsMap(connection).get("user")); + } + finally + { + connection.disconnect(); + } + assertEquals(CLIENT_PRINCIPAL_FULL_NAME, getSessionUser(cookie)); + } + + private String prepareSession() throws Exception + { + final HttpURLConnection connection = _kerberosHelper.openManagementConnection(SASL_SERVICE, "GET"); + try + { + assertEquals(SC_OK, connection.getResponseCode()); + assertNull(_kerberosHelper.readJsonResponseAsMap(connection).get("user")); + return getSessionCookie(connection); + } + finally + { + connection.disconnect(); + } + } + + private String authenticate(final String cookie) throws Exception + { + final byte[] token = UTILS.buildToken(CLIENT_PRINCIPAL_NAME, _clientKeyTabFile, SERVICE_PRINCIPAL_NAME); + final HttpURLConnection connection = _kerberosHelper + .openManagementConnection(HttpManagement.DEFAULT_LOGIN_URL, "GET"); + try + { + connection.setInstanceFollowRedirects(false); + if (cookie != null) + { + connection.setRequestProperty("Cookie", cookie); + } + connection.setRequestProperty(SpnegoAuthenticator.REQUEST_AUTH_HEADER_NAME, + SpnegoAuthenticator.RESPONSE_AUTH_HEADER_VALUE_NEGOTIATE + " " + + Base64.getEncoder().encodeToString(token)); + assertEquals(SC_OK, connection.getResponseCode()); + return getSessionCookie(connection); + } + finally + { + connection.disconnect(); + } + } + + private Object getSessionUser(final String cookie) throws Exception + { + final HttpURLConnection connection = _kerberosHelper.openManagementConnection(SASL_SERVICE, "GET"); + try + { + connection.setRequestProperty("Cookie", cookie); + assertEquals(SC_OK, connection.getResponseCode()); + return _kerberosHelper.readJsonResponseAsMap(connection).get("user"); + } + finally + { + connection.disconnect(); + } + } + + private String getSessionCookie(final HttpURLConnection connection) + { + final String cookie = connection.getHeaderField("Set-Cookie"); + assertNotNull(cookie, "Expected a management session cookie"); + return cookie.split(";", 2)[0]; + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
