/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file 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.picketlink.test.identity.federation.bindings.workflow;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.realm.GenericPrincipal;
import org.junit.Test;
import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.bindings.tomcat.idp.IDPWebBrowserSSOValve;
import org.picketlink.identity.federation.bindings.tomcat.sp.SPRedirectFormAuthenticator;
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
import org.picketlink.identity.federation.saml.v2.protocol.StatusResponseType;
import org.picketlink.identity.federation.web.constants.GeneralConstants;
import org.picketlink.identity.federation.web.core.IdentityParticipantStack;
import org.picketlink.identity.federation.web.core.IdentityServer;
import org.picketlink.identity.federation.web.util.RedirectBindingUtil;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaContext;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaContextClassLoader;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaLoginConfig;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaRealm;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaRequest;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaResponse;
import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaSession;
/**
* Unit test the SAML2 Logout Mechanism for Tomcat bindings
*
* @author Anil.Saldhana@redhat.com
* @since Oct 21, 2009
*/
@SuppressWarnings("unused")
public class SAML2LogoutTomcatWorkflowUnitTestCase {
private final String profile = "saml2/logout";
private ClassLoader tcl;
private final String IDP = "http://localhost:8080/idp/";
private final String employee = "http://localhost:8080/employee/";
private final String sales = "http://localhost:8080/sales/";
private final String RELAY_STATE_KEY = "RelayState=";
private final String SAML_REQUEST_KEY = "SAMLRequest=";
private final String SAML_RESPONSE_KEY = "SAMLResponse=";
/**
* Test that the SP Redirect Authenticator generates the logout request to the IDP when there is a parameter "GLO" set to
* true
*
* @see {@code GeneralConstants#GLOBAL_LOGOUT}
* @throws Exception
*/
@Test
public void testSPLogOutRequestGeneration() throws Exception {
System.setProperty("picketlink.schema.validate", "true");
MockCatalinaSession session = new MockCatalinaSession();
Principal principal = new Principal() {
public String getName() {
return "anil";
}
};
session.setAttribute(GeneralConstants.PRINCIPAL_ID, principal);
List<String> rolesList = new ArrayList<String>();
rolesList.add("manager");
session.setAttribute(GeneralConstants.ROLES_ID, rolesList);
MockCatalinaContext context = new MockCatalinaContext();
session.setServletContext(context);
// Let us feed the LogOutRequest to the SPFilter
MockCatalinaContextClassLoader mclSPEmp = setupTCL(profile + "/sp/employee");
Thread.currentThread().setContextClassLoader(mclSPEmp);
SPRedirectFormAuthenticator sp = new SPRedirectFormAuthenticator();
sp.setContainer(context);
sp.testStart();
sp.getConfiguration().setIdpUsesPostBinding(false);
MockCatalinaRequest catalinaRequest = new MockCatalinaRequest();
catalinaRequest.setSession(new MockCatalinaSession());
catalinaRequest.setUserPrincipal(principal);
MockCatalinaResponse response = new MockCatalinaResponse();
MockCatalinaLoginConfig loginConfig = new MockCatalinaLoginConfig();
ByteArrayOutputStream filterbaos = new ByteArrayOutputStream();
response.setWriter(new PrintWriter(filterbaos));
catalinaRequest.setParameter(GeneralConstants.GLOBAL_LOGOUT, "true");
sp.authenticate(catalinaRequest, response, loginConfig);
String redirectStr = response.redirectString;
String logoutRequest = redirectStr.substring(redirectStr.indexOf(SAML_REQUEST_KEY) + SAML_REQUEST_KEY.length());
InputStream stream = RedirectBindingUtil.urlBase64DeflateDecode(logoutRequest);
SAML2Request saml2Request = new SAML2Request();
LogoutRequestType lor = (LogoutRequestType) saml2Request.getRequestType(stream);
assertEquals("Match Employee URL", employee, lor.getIssuer().getValue());
}
@SuppressWarnings("deprecation")
@Test
public void testSAML2LogOutFromIDP() throws Exception {
System.setProperty("picketlink.schema.validate", "true");
MockCatalinaSession session = new MockCatalinaSession();
MockCatalinaContextClassLoader mclIDP = setupTCL(profile + "/idp");
Thread.currentThread().setContextClassLoader(mclIDP);
MockCatalinaContext catalinaContext = new MockCatalinaContext();
session.setServletContext(catalinaContext);
IdentityServer server = this.getIdentityServer(session);
catalinaContext.setAttribute("IDENTITY_SERVER", server);
IDPWebBrowserSSOValve idp = new IDPWebBrowserSSOValve();
idp.setContainer(catalinaContext);
idp.setSignOutgoingMessages(false);
idp.setIgnoreIncomingSignatures(true);
idp.setStrictPostBinding(false);
idp.start();
// Assume that we already have the principal and roles set in the session
MockCatalinaRealm realm = new MockCatalinaRealm("anil", "test", new Principal() {
public String getName() {
return "anil";
}
});
List<String> roles = new ArrayList<String>();
roles.add("manager");
roles.add("employee");
List<String> rolesList = new ArrayList<String>();
rolesList.add("manager");
MockCatalinaRequest request = new MockCatalinaRequest();
session.clear();
request.setSession(session);
request.addHeader("Referer", sales);
GenericPrincipal genericPrincipal = new GenericPrincipal(realm, "anil", "test", roles);
request.setUserPrincipal(genericPrincipal);
//We start the workflow with the sales application sending a logout request
String samlMessage = RedirectBindingUtil.deflateBase64Encode(createLogOutRequest(sales).getBytes());
request.setParameter("SAMLRequest", samlMessage);
MockCatalinaResponse response = new MockCatalinaResponse();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
response.setWriter(new PrintWriter(baos));
// The IDP is preloaded with 2 participants : "http://localhost:8080/sales/"
// and "http://localhost:8080/employee"
// Lets start the workflow with get
request.setMethod("GET");
idp.invoke(request, response);
String redirectStr = response.redirectString;
String destination = redirectStr.substring(0, redirectStr.indexOf(SAML_REQUEST_KEY) - 1);
String relayState = redirectStr.substring(redirectStr.indexOf(RELAY_STATE_KEY) + RELAY_STATE_KEY.length());
String logoutRequest = redirectStr.substring(redirectStr.indexOf(SAML_REQUEST_KEY) + SAML_REQUEST_KEY.length(),
redirectStr.indexOf(RELAY_STATE_KEY) - 1);
InputStream stream = RedirectBindingUtil.urlBase64DeflateDecode(logoutRequest);
SAML2Request saml2Request = new SAML2Request();
LogoutRequestType lor = (LogoutRequestType) saml2Request.getRequestType(stream);
assertEquals("Match Employee URL", employee, destination);
assertEquals("Destination exists", employee, lor.getDestination().toString());
// IDP has sent a LogOutRequest which we feed to SPRedirectFormAuthenticator for Employee
MockCatalinaContextClassLoader mclSPEmp = setupTCL(profile + "/sp/employee");
Thread.currentThread().setContextClassLoader(mclSPEmp);
MockCatalinaContext context = new MockCatalinaContext();
context.setRealm(realm);
session.setServletContext(context);
SPRedirectFormAuthenticator sp = new SPRedirectFormAuthenticator();
sp.setContainer(context);
sp.testStart();
sp.getConfiguration().setIdpUsesPostBinding(false);
request = new MockCatalinaRequest();
request.setSession(session);
request.setMethod("GET");
request.setParameter("SAMLRequest", RedirectBindingUtil.urlDecode(logoutRequest));
request.setParameter("RelayState", relayState);
MockCatalinaResponse filterResponse = new MockCatalinaResponse();
ByteArrayOutputStream filterbaos = new ByteArrayOutputStream();
filterResponse.setWriter(new PrintWriter(filterbaos));
sp.authenticate(request, response, new LoginConfig());
redirectStr = response.redirectString;
destination = redirectStr.substring(0, redirectStr.indexOf(SAML_RESPONSE_KEY) - 1);
relayState = redirectStr.substring(redirectStr.indexOf(RELAY_STATE_KEY) + RELAY_STATE_KEY.length());
assertNotNull("RelayState exists", relayState);
String logoutResponse = redirectStr.substring(redirectStr.indexOf(SAML_RESPONSE_KEY) + SAML_RESPONSE_KEY.length(),
redirectStr.indexOf(RELAY_STATE_KEY) - 1);
stream = RedirectBindingUtil.urlBase64DeflateDecode(logoutResponse);
StatusResponseType statusResponse = (StatusResponseType) saml2Request.getSAML2ObjectFromStream(stream);
assertEquals("Match IDP URL", IDP, destination);
// Now the SP (employee app) has logged out and sending a status response to IDP
Thread.currentThread().setContextClassLoader(mclIDP);
session.clear();
request.clear();
request.setMethod("GET");
request.setSession(session);
request.setUserPrincipal(genericPrincipal);
request.setParameter("SAMLResponse", RedirectBindingUtil.urlDecode(logoutResponse));
request.setParameter("RelayState", relayState);
baos = new ByteArrayOutputStream();
response.setOutputStream(baos);
response.setWriter(new PrintWriter(baos));
idp.invoke(request, response);
destination = redirectStr.substring(0, redirectStr.indexOf(SAML_RESPONSE_KEY) - 1);
relayState = redirectStr.substring(redirectStr.indexOf(RELAY_STATE_KEY) + RELAY_STATE_KEY.length());
logoutResponse = redirectStr.substring(redirectStr.indexOf(SAML_RESPONSE_KEY) + SAML_RESPONSE_KEY.length(),
redirectStr.indexOf(RELAY_STATE_KEY) - 1);
stream = RedirectBindingUtil.urlBase64DeflateDecode(logoutResponse);
SAML2Response saml2Response = new SAML2Response();
statusResponse = (StatusResponseType) saml2Request.getSAML2ObjectFromStream(stream);
assertEquals("Match IDP URL", IDP, destination);
// Now we should have got a full success report from IDP
MockCatalinaContextClassLoader mclSPSales = setupTCL(profile + "/sp/employee");
Thread.currentThread().setContextClassLoader(mclSPSales);
sp = new SPRedirectFormAuthenticator();
sp.setContainer(context);
sp.testStart();
session.clear();
request.clear();
request.setSession(session);
request.setUserPrincipal(genericPrincipal);
request.setParameter("SAMLResponse", RedirectBindingUtil.urlDecode(logoutResponse));
request.setParameter("RelayState", relayState);
request.setContext(context);
//IDP should now send the final logout response to Sales application who sent the original request
sp.authenticate(request, response, new LoginConfig());
//Ensure that at the IDP we do not have any participants in the session (both employee and sales are logged out)
IdentityParticipantStack stack = server.stack();
assertEquals(0, stack.getParticipants(session.getId()));
assertEquals(0, stack.getNumOfParticipantsInTransit(session.getId()));
// Finally the session should be invalidated
assertTrue(session.isInvalidated());
}
private MockCatalinaContextClassLoader setupTCL(String resource) {
tcl = Thread.currentThread().getContextClassLoader();
URL[] urls = new URL[] { tcl.getResource(resource) };
MockCatalinaContextClassLoader mcl = new MockCatalinaContextClassLoader(urls);
mcl.setDelegate(tcl);
mcl.setProfile(resource);
return mcl;
}
private String createLogOutRequest(String url) throws Exception {
SAML2Request samlRequest = new SAML2Request();
LogoutRequestType lot = samlRequest.createLogoutRequest(url);
Principal userPrincipal = new Principal() {
@Override
public String getName() {
return "test";
}
};
NameIDType nameID = new NameIDType();
nameID.setValue(userPrincipal.getName());
lot.setNameID(nameID);
StringWriter sw = new StringWriter();
samlRequest.marshall(lot, sw);
return sw.toString();
}
// Get the Identity server with 2 participants
private IdentityServer getIdentityServer(HttpSession session) {
IdentityServer server = new IdentityServer();
server.sessionCreated(new HttpSessionEvent(session));
server.stack().register(session.getId(), sales, false);
server.stack().register(session.getId(), employee, false);
return server;
}
}