/* * JBoss, a division of Red Hat * Copyright 2006, Red Hat Middleware, LLC, and individual contributors as indicated * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.test.integration.web.sso; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_ATTRIBUTE_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE; import static org.jboss.as.test.integration.management.util.ModelUtil.createOpNode; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.HttpClientUtils; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.cookie.BasicClientCookie; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.OperationBuilder; import org.jboss.as.test.http.util.TestHttpClientUtils; import org.jboss.as.test.integration.web.sso.interfaces.StatelessSession; import org.jboss.as.test.shared.RetryTaskExecutor; import org.jboss.dmr.ModelNode; import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.EnterpriseArchive; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; /** * Base class for tests of web app single sign-on * * @author Brian Stansberry * @author lbarreiro@redhat.com */ public abstract class SSOTestBase { private static Logger log = Logger.getLogger(SSOTestBase.class); /** * Test single sign-on across two web apps using form based auth * * @throws Exception */ public static void executeFormAuthSingleSignOnTest(URL serverA, URL serverB, Logger log) throws Exception { URL warA1 = new URL(serverA, "/war1/"); URL warB2 = new URL(serverB, "/war2/"); // Start by accessing the secured index.html of war1 CookieStore store = new BasicCookieStore(); HttpClient httpclient = TestHttpClientUtils.promiscuousCookieHttpClientBuilder() .setDefaultCookieStore(store) .disableRedirectHandling() .build(); try { checkAccessDenied(httpclient, warA1 + "index.html"); log.debug("Saw JSESSIONID=" + getSessionIdValueFromState(store)); // Submit the login form executeFormLogin(httpclient, warA1); String ssoID = processSSOCookie(store, serverA.toString(), serverB.toString()); log.debug("Saw JSESSIONIDSSO=" + ssoID); // Now try getting the war2 index using the JSESSIONIDSSO cookie log.debug("Prepare /war2/index.html get"); checkAccessAllowed(httpclient, warB2 + "index.html"); // Access a secured servlet that calls a secured ejb in war2 to test // propagation of the SSO identity to the ejb container. checkAccessAllowed(httpclient, warB2 + "EJBServlet"); // Now try logging out of war2 executeLogout(httpclient, warB2); } finally { HttpClientUtils.closeQuietly(httpclient); } try { // Reset Http client httpclient = HttpClients.createDefault(); // Try accessing war1 again checkAccessDenied(httpclient, warA1 + "index.html"); // Try accessing war2 again checkAccessDenied(httpclient, warB2 + "index.html"); } finally { HttpClientUtils.closeQuietly(httpclient); } } public static void executeNoAuthSingleSignOnTest(URL serverA, URL serverB, Logger log) throws Exception { URL warA1 = new URL(serverA, "/war1/"); URL warB2 = new URL(serverB + "/war2/"); URL warB6 = new URL(serverB + "/war6/"); // Start by accessing the secured index.html of war1 CookieStore store = new BasicCookieStore(); HttpClient httpclient = TestHttpClientUtils.promiscuousCookieHttpClientBuilder().setDefaultCookieStore(store).build(); try { checkAccessDenied(httpclient, warA1 + "index.html"); log.debug("Saw JSESSIONID=" + getSessionIdValueFromState(store)); // Submit the login form executeFormLogin(httpclient, warA1); String ssoID = processSSOCookie(store, serverA.toString(), serverB.toString()); log.debug("Saw JSESSIONIDSSO=" + ssoID); // Now try getting the war2 index using the JSESSIONIDSSO cookie log.debug("Prepare /war2/index.html get"); checkAccessAllowed(httpclient, warB2 + "index.html"); // Access a secured servlet that calls a secured ejb in war2 to test // propagation of the SSO identity to the ejb container. checkAccessAllowed(httpclient, warB2 + "EJBServlet"); // Do the same test on war6 to test SSO auth replication with no auth // configured war checkAccessAllowed(httpclient, warB6 + "index.html"); checkAccessAllowed(httpclient, warB2 + "EJBServlet"); } finally { HttpClientUtils.closeQuietly(httpclient); } } /** * Test single sign-on across two web apps using form based auth. * * Test that after session timeout SSO is destroyed. * * @throws Exception */ public static void executeFormAuthSSOTimeoutTest(URL serverA, URL serverB, Logger log) throws Exception { URL warA1 = new URL(serverA, "/war1/"); URL warB2 = new URL(serverB, "/war2/"); // Start by accessing the secured index.html of war1 CookieStore store = new BasicCookieStore(); HttpClient httpclient = TestHttpClientUtils.promiscuousCookieHttpClientBuilder() .setDefaultCookieStore(store) .disableRedirectHandling() .build(); try { checkAccessDenied(httpclient, warA1 + "index.html"); log.debug("Saw JSESSIONID=" + getSessionIdValueFromState(store)); // Submit the login form executeFormLogin(httpclient, warA1); String ssoID = processSSOCookie(store, serverA.toString(), serverB.toString()); log.debug("Saw JSESSIONIDSSO=" + ssoID); // After login I should still have access + set session timeout to 5 seconds checkAccessAllowed(httpclient, warA1 + "set_session_timeout.jsp"); // Also access to war2 should be granted + set session timeout to 5 seconds checkAccessAllowed(httpclient, warB2 + "set_session_timeout.jsp"); // wait 5 seconds session timeout + 1 seconds reserve Thread.sleep((5+1)*1000); // After timeout I should be not able to access the app checkAccessDenied(httpclient, warA1 + "index.html"); checkAccessDenied(httpclient, warB2 + "index.html"); } finally { HttpClientUtils.closeQuietly(httpclient); } } public static void executeLogout(HttpClient httpConn, URL warURL) throws IOException { HttpGet logout = new HttpGet(warURL + "Logout"); HttpResponse response = httpConn.execute(logout); try { int statusCode = response.getStatusLine().getStatusCode(); assertTrue("Logout: Didn't see code 302 (HTTP_MOVED_TEMP), but saw instead " + statusCode, statusCode == HttpURLConnection.HTTP_MOVED_TEMP); Header location = response.getFirstHeader("Location"); assertTrue("Get of " + warURL + "Logout not redirected to login page", location.getValue().contains("index.html")); } finally { HttpClientUtils.closeQuietly(response); } } public static void checkAccessAllowed(HttpClient httpConn, String url) throws IOException { HttpGet getMethod = new HttpGet(url); HttpResponse response = httpConn.execute(getMethod); try { int statusCode = response.getStatusLine().getStatusCode(); assertTrue("Expected code == OK but got " + statusCode + " for request=" + url, statusCode == HttpURLConnection.HTTP_OK); String body = EntityUtils.toString(response.getEntity()); assertTrue("Get of " + url + " redirected to login page", !body.contains("j_security_check")); } finally { HttpClientUtils.closeQuietly(response); } } public static void executeFormLogin(HttpClient httpConn, URL warURL) throws IOException { // Submit the login form HttpPost formPost = new HttpPost(warURL + "j_security_check"); formPost.addHeader("Referer", warURL + "login.html"); List<NameValuePair> formparams = new ArrayList<>(); formparams.add(new BasicNameValuePair("j_username", "user1")); formparams.add(new BasicNameValuePair("j_password", "password1")); formPost.setEntity(new UrlEncodedFormEntity(formparams, "UTF-8")); HttpResponse postResponse = httpConn.execute(formPost); try { int statusCode = postResponse.getStatusLine().getStatusCode(); Header[] errorHeaders = postResponse.getHeaders("X-NoJException"); assertTrue("Should see HTTP_MOVED_TEMP. Got " + statusCode, statusCode == HttpURLConnection.HTTP_MOVED_TEMP); assertTrue("X-NoJException(" + Arrays.toString(errorHeaders) + ") is null", errorHeaders.length == 0); EntityUtils.consume(postResponse.getEntity()); // Follow the redirect to the index.html page String indexURL = postResponse.getFirstHeader("Location").getValue(); HttpGet rediretGet = new HttpGet(indexURL); HttpResponse redirectResponse = httpConn.execute(rediretGet); statusCode = redirectResponse.getStatusLine().getStatusCode(); errorHeaders = redirectResponse.getHeaders("X-NoJException"); assertTrue("Wrong response code: " + statusCode, statusCode == HttpURLConnection.HTTP_OK); assertTrue("X-NoJException(" + Arrays.toString(errorHeaders) + ") is null", errorHeaders.length == 0); String body = EntityUtils.toString(redirectResponse.getEntity()); assertTrue("Get of " + indexURL + " redirected to login page", !body.contains("j_security_check")); } finally { HttpClientUtils.closeQuietly(postResponse); } } public static void checkAccessDenied(HttpClient httpConn, String url) throws IOException { HttpGet getMethod = new HttpGet(url); HttpResponse response = httpConn.execute(getMethod); try { int statusCode = response.getStatusLine().getStatusCode(); assertTrue("Expected code == OK but got " + statusCode + " for request=" + url, statusCode == HttpURLConnection.HTTP_OK); String body = EntityUtils.toString(response.getEntity()); assertTrue("Redirected to login page for request=" + url + ", body[" + body + "]", body.indexOf("j_security_check") > 0); } finally { HttpClientUtils.closeQuietly(response); } } public static String processSSOCookie(CookieStore cookieStore, String serverA, String serverB) { String ssoID = null; for (Cookie cookie : cookieStore.getCookies()) { if ("JSESSIONIDSSO".equalsIgnoreCase(cookie.getName())) { ssoID = cookie.getValue(); if (!serverA.equals(serverB)) { // Make an sso cookie to send to serverB Cookie copy = copyCookie(cookie, serverB); cookieStore.addCookie(copy); } } } assertTrue("Didn't see JSESSIONIDSSO: " + cookieStore.getCookies(), ssoID != null); return ssoID; } public static Cookie copyCookie(Cookie toCopy, String targetServer) { // Parse the target server down to a domain name int index = targetServer.indexOf("://"); if (index > -1) { targetServer = targetServer.substring(index + 3); } // JBAS-8540 // need to be able to parse IPv6 URLs which have enclosing brackets // HttpClient 3.1 creates cookies which include the square brackets // index = targetServer.indexOf(":"); index = targetServer.lastIndexOf(":"); if (index > -1) { targetServer = targetServer.substring(0, index); } index = targetServer.indexOf("/"); if (index > -1) { targetServer = targetServer.substring(0, index); } // Cookie copy = new Cookie(targetServer, toCopy.getName(), toCopy.getValue(), "/", null, false); BasicClientCookie copy = new BasicClientCookie(toCopy.getName(), toCopy.getValue()); copy.setDomain(targetServer); return copy; } public static String getSessionIdValueFromState(CookieStore cookieStore) { String sessionID = null; for (Cookie cookie : cookieStore.getCookies()) { if ("JSESSIONID".equalsIgnoreCase(cookie.getName())) { sessionID = cookie.getValue(); break; } } return sessionID; } public static WebArchive createSsoWar(String warName) { ClassLoader tccl = Thread.currentThread().getContextClassLoader(); String resourcesLocation = "org/jboss/as/test/integration/web/sso/resources/"; WebArchive war = ShrinkWrap.create(WebArchive.class, warName); war.setWebXML(tccl.getResource(resourcesLocation + "web-form-auth.xml")); war.addAsWebInfResource(tccl.getResource(resourcesLocation + "jboss-web.xml"), "jboss-web.xml"); war.addAsWebResource(tccl.getResource(resourcesLocation + "error.html"), "error.html"); war.addAsWebResource(tccl.getResource(resourcesLocation + "index.html"), "index.html"); war.addAsWebResource(tccl.getResource(resourcesLocation + "index.jsp"), "index.jsp"); war.addAsWebResource(tccl.getResource(resourcesLocation + "set_session_timeout.jsp"), "set_session_timeout.jsp"); war.addAsWebResource(tccl.getResource(resourcesLocation + "login.html"), "login.html"); war.addClass(EJBServlet.class); war.addClass(LogoutServlet.class); return war; } public static EnterpriseArchive createSsoEar() { ClassLoader tccl = Thread.currentThread().getContextClassLoader(); String resourcesLocation = "org/jboss/as/test/integration/web/sso/resources/"; WebArchive war1 = createSsoWar("sso-form-auth1.war"); WebArchive war2 = createSsoWar("sso-form-auth2.war"); WebArchive war3 = createSsoWar("sso-with-no-auth.war"); // Remove jboss-web.xml so the war will not have an authenticator war3.delete(war3.get("WEB-INF/jboss-web.xml").getPath()); JavaArchive webEjbs = ShrinkWrap.create(JavaArchive.class, "jbosstest-web-ejbs.jar"); webEjbs.addAsManifestResource(tccl.getResource(resourcesLocation + "ejb-jar.xml"), "ejb-jar.xml"); webEjbs.addAsManifestResource(tccl.getResource(resourcesLocation + "jboss.xml"), "jboss.xml"); webEjbs.addPackage(StatelessSession.class.getPackage()); EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "web-sso.ear"); ear.setApplicationXML(tccl.getResource(resourcesLocation + "application.xml")); ear.addAsModule(war1); ear.addAsModule(war2); ear.addAsModule(war3); ear.addAsModule(webEjbs); return ear; } public static void addSso(ModelControllerClient client) throws Exception { final List<ModelNode> updates = new ArrayList<>(); // SSO element name must be 'configuration' updates.add(createOpNode("subsystem=undertow/server=default-server/host=default-host/setting=single-sign-on", ADD)); applyUpdates(updates, client); } public static void removeSso(final ModelControllerClient client) throws Exception { final List<ModelNode> updates = new ArrayList<>(); updates.add(createOpNode("subsystem=undertow/server=default-server/host=default-host/setting=single-sign-on", REMOVE)); applyUpdates(updates, client); } public static void applyUpdates(final List<ModelNode> updates, final ModelControllerClient client) throws Exception { for (ModelNode update : updates) { //log.trace("+++ Update on " + client + ":\n" + update.toString()); ModelNode result = client.execute(new OperationBuilder(update).build()); if (result.hasDefined("outcome") && "success".equals(result.get("outcome").asString())) { if (result.hasDefined("result")) log.trace(result.get("result")); } else if (result.hasDefined("failure-description")) { throw new RuntimeException(result.get("failure-description").toString()); } else { throw new RuntimeException("Operation not successful; outcome = " + result.get("outcome")); } } } // Reload operation is not handled well by Arquillian // See ARQ-791: JMX: Arquillian is unable to reconnect to JMX server if the connection is lost public static void restartServer(final ModelControllerClient client) { try { applyUpdates(Arrays.asList(createOpNode(null, "reload")), client); } catch (Exception e) { throw new RuntimeException("Restart operation not successful. " + e.getMessage()); } try { RetryTaskExecutor<Boolean> rte = new RetryTaskExecutor<>(); rte.retryTask(new Callable<Boolean>() { public Boolean call() throws Exception { ModelNode readOp = createOpNode(null, READ_ATTRIBUTE_OPERATION); readOp.get("name").set("server-state"); ModelNode result = client.execute(new OperationBuilder(readOp).build()); if (result.hasDefined("outcome") && "success".equals(result.get("outcome").asString())) { if ((result.hasDefined("result")) && (result.get("result").asString().equals("running"))) return true; } log.trace("Server is down."); throw new Exception("Connector not available."); } }); } catch (TimeoutException e) { throw new RuntimeException("Timeout on restart operation. " + e.getMessage()); } log.trace("Server is up."); } }