package org.jboss.seam.security.externaltest.integration.client; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.framework.Assert; import org.apache.commons.httpclient.HttpStatus; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; 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.methods.HttpUriRequest; import org.apache.http.client.params.ClientPNames; import org.apache.http.client.utils.URIUtils; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.OperateOnDeployment; import org.jboss.arquillian.container.test.api.RunAsClient; import org.jboss.arquillian.junit.Arquillian; import org.jboss.solder.logging.Logger; import org.jboss.shrinkwrap.api.Archive; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(Arquillian.class) @RunAsClient public class IntegrationTest { private static final Logger log = Logger.getLogger(IntegrationTest.class); private HttpClient httpClient; private HttpUriRequest request; private ResponseType responseType; private String responseBody; private HttpResponse response; enum ResponseType { SAML_MESSAGE_REDIRECT_BOUND, SAML_MESSAGE_POST_BOUND, APPLICATION_MESSAGE, ERROR } @Deployment(order = 1, name = "sp") public static Archive<?> createSpArchive() { return ArchiveBuilder.getArchive("sp"); } @Deployment(order = 2, name = "idp") public static Archive<?> createIdpArchive() { return ArchiveBuilder.getArchive("idp"); } @Deployment(order = 3, name = "rp") public static Archive<?> createRpArchive() { return ArchiveBuilder.getArchive("rp"); } @Deployment(order = 4, name = "op") public static Archive<?> createOpArchive() { return ArchiveBuilder.getArchive("op"); } // Seems broken in ARQ Alpha5 //@Before public void init() { httpClient = new DefaultHttpClient(); httpClient.getParams().setParameter(ClientPNames.HANDLE_REDIRECTS, false); } @Test @OperateOnDeployment("sp") public void samlTest() { init(); Map<String, String> params = new HashMap<String, String>(); params.put("command", "loadMetaData"); sendMessageToApplication("www.sp1.com", "sp", params); sendMessageToApplication("www.sp2.com", "sp", params); sendMessageToApplication("www.idp.com", "idp", params); // Login one user at each service provider application samlSignOn("www.sp1.com", "https://www.idp.com", "John Doe"); samlSignOn("www.sp2.com", "https://www.idp.com", "Jane Doe"); // Check that the IDP has two sessions (one for each user) and that each // SP has one checkNrOfSessions("www.idp.com", "idp", 2); checkNrOfSessions("www.sp1.com", "sp", 1); checkNrOfSessions("www.sp2.com", "sp", 1); // Do an IDP-initiated single logout of the user at SP1. params.clear(); params.put("command", "singleLogout"); sendMessageToApplication("www.idp.com", "idp", params); checkApplicationMessage("Single logout succeeded"); checkNrOfSessions("www.idp.com", "idp", 1); checkNrOfSessions("www.sp1.com", "sp", 0); checkNrOfSessions("www.sp2.com", "sp", 1); // Do an SP-initiated single logout of the user at SP2. params.clear(); params.put("command", "singleLogout"); sendMessageToApplication("www.sp2.com", "sp", params); checkApplicationMessage("Single logout succeeded"); // All sessions should be terminated by now. checkNrOfSessions("www.idp.com", "idp", 0); checkNrOfSessions("www.sp1.com", "sp", 0); checkNrOfSessions("www.sp2.com", "sp", 0); // All dialogues should be terminated by now. checkDialogueTermination("www.idp.com", "idp"); checkDialogueTermination("www.sp1.com", "sp"); checkDialogueTermination("www.sp2.com", "sp"); } @Test @OperateOnDeployment("sp") public void openIdLoginWithOpIdentifierTest() { init(); String opIdentifier = "http://localhost:8080/op/openid/OP/XrdsService"; String userName = "john_doe"; Map<String, String> params = new HashMap<String, String>(); params.put("command", "login"); params.put("identifier", opIdentifier); params.put("fetchEmail", "false"); sendMessageToApplication("localhost", "rp", params); checkApplicationMessage("Please login."); params = new HashMap<String, String>(); params.put("command", "authenticate"); params.put("userName", userName); sendMessageToApplication("localhost", "op", params); checkApplicationMessage("Login succeeded (http://localhost:8080/op/users/" + userName + ")"); // All dialogues should be terminated by now. checkDialogueTermination("www.op.com", "op"); checkDialogueTermination("www.rp.com", "rp"); } @Test @OperateOnDeployment("sp") public void openIdLoginWithClaimedIdentifierAndAttributeExchangeTest() { init(); String userName = "jane_doe"; String claimedId = "http://localhost:8080/op/users/" + userName; Map<String, String> params = new HashMap<String, String>(); params.put("command", "login"); params.put("identifier", claimedId); params.put("fetchEmail", "true"); sendMessageToApplication("localhost", "rp", params); checkApplicationMessage("Please provide the password for " + userName + "."); params = new HashMap<String, String>(); params.put("command", "authenticate"); params.put("userName", userName); sendMessageToApplication("localhost", "op", params); checkApplicationMessage("Please provide your email."); params = new HashMap<String, String>(); params.put("command", "setAttribute"); String email = "jane_doe@op.com"; params.put("email", email); sendMessageToApplication("localhost", "op", params); checkApplicationMessage("Login succeeded (" + claimedId + ", email " + email + ")"); // All dialogues should be terminated by now. checkDialogueTermination("www.op.com", "op"); checkDialogueTermination("www.rp.com", "rp"); } private void checkNrOfSessions(String serverName, String spOrIdp, int expectedNumber) { Map<String, String> params = new HashMap<String, String>(); params.put("command", "getNrOfSessions"); sendMessageToApplication(serverName, spOrIdp, params); checkApplicationMessage(Integer.toString(expectedNumber)); } private void samlSignOn(String spHostName, String idpEntityId, String userName) { Map<String, String> params = new HashMap<String, String>(); params.put("command", "login"); params.put("idpEntityId", idpEntityId); sendMessageToApplication(spHostName, "sp", params); checkApplicationMessage("Please login"); params = new HashMap<String, String>(); params.put("command", "authenticate"); params.put("userName", userName); sendMessageToApplication("www.idp.com", "idp", params); checkApplicationMessage("Login succeeded (" + userName + ")"); } private void sendMessageToApplication(String hostName, String contextRoot, Map<String, String> params) { List<NameValuePair> qParams = new ArrayList<NameValuePair>(); for (Map.Entry<String, String> mapEntry : params.entrySet()) { qParams.add(new BasicNameValuePair(mapEntry.getKey(), mapEntry.getValue())); } URI uri; try { uri = URIUtils.createURI("http", "localhost", 8080, "/" + contextRoot + "/testservlet", URLEncodedUtils.format(qParams, "UTF-8"), null); } catch (URISyntaxException e) { throw new RuntimeException(e); } request = new HttpGet(uri); if (!hostName.equals("localhost")) { request.getParams().setParameter(ClientPNames.VIRTUAL_HOST, new HttpHost(hostName, 8080)); } executeHttpRequestAndRelay(); } private void checkDialogueTermination(String serverName, String spOrIdp) { Map<String, String> params = new HashMap<String, String>(); params.put("command", "getNrOfDialogues"); sendMessageToApplication(serverName, spOrIdp, params); checkApplicationMessage("0"); } /** * Relays the SAML message from the SP to the IDP or vice versa. Results in * an HTTP request that is ready to be executed. */ private void relaySamlMessage() { if (responseType == ResponseType.SAML_MESSAGE_POST_BOUND) { Matcher matcher = Pattern.compile("ACTION=\"(.*?)\"").matcher(responseBody); matcher.find(); String uri = matcher.group(1); matcher = Pattern.compile("NAME=\"(.*?)\"").matcher(responseBody); matcher.find(); String name = matcher.group(1); matcher = Pattern.compile("VALUE=\"(.*?)\"").matcher(responseBody); matcher.find(); String value = matcher.group(1); String serverName = extractServerNameFromUri(uri); uri = uri.replace(serverName, "localhost"); HttpPost httpPost = new HttpPost(uri); if (!serverName.equals("localhost")) { httpPost.getParams().setParameter(ClientPNames.VIRTUAL_HOST, new HttpHost(serverName, 8080)); } List<NameValuePair> formparams = new ArrayList<NameValuePair>(); formparams.add(new BasicNameValuePair(name, value)); UrlEncodedFormEntity entity; try { entity = new UrlEncodedFormEntity(formparams, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } httpPost.setEntity(entity); request = httpPost; } else if (responseType == ResponseType.SAML_MESSAGE_REDIRECT_BOUND) { String location = response.getFirstHeader("Location").getValue(); log.info("Received redirect to " + location); String serverName = extractServerNameFromUri(location); HttpGet httpGet = new HttpGet(location.replace(serverName, "localhost")); httpGet.getParams().setParameter(ClientPNames.VIRTUAL_HOST, new HttpHost(serverName, 8080)); request = httpGet; } else if (responseType == ResponseType.ERROR) { Assert.fail("Error response received by test client (status code " + response.getStatusLine().getStatusCode() + "): " + responseBody); } else { throw new RuntimeException("Cannot relay the non-SAML response type " + responseType + " (message: " + responseBody + ")"); } } private ResponseType determineResponseType() { if (response.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { return ResponseType.SAML_MESSAGE_REDIRECT_BOUND; } else if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { return ResponseType.ERROR; } else if (responseBody.contains("HTTP Post SamlBinding")) { return ResponseType.SAML_MESSAGE_POST_BOUND; } else { return ResponseType.APPLICATION_MESSAGE; } } private String extractServerNameFromUri(String string) { Matcher matcher = Pattern.compile("http://(.*?):").matcher(string); matcher.find(); return matcher.group(1); } private void checkApplicationMessage(String expectedMessageBody) { if (responseType == ResponseType.ERROR) { Assert.fail("Error response received by test client (status code " + response.getStatusLine().getStatusCode() + "): " + responseBody); } Assert.assertEquals(ResponseType.APPLICATION_MESSAGE, responseType); Assert.assertEquals(expectedMessageBody, responseBody); } /** * Executes the current HTTP request and evaluates the response. If the * response is a SAML message that needs to be relayed, by the user agent * (which is mimicked by the current class), from the SP to the IDP or vice * versa, the relay is performed. This is repeated until a non-relay response * has been received. */ private void executeHttpRequestAndRelay() { executeHttpRequest(); while (responseType == ResponseType.SAML_MESSAGE_POST_BOUND || responseType == ResponseType.SAML_MESSAGE_REDIRECT_BOUND) { relaySamlMessage(); executeHttpRequest(); } } private void executeHttpRequest() { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { response = httpClient.execute(request); HttpEntity entity = response.getEntity(); if (entity != null) { entity.writeTo(outputStream); } responseBody = outputStream.toString("UTF-8"); } catch (IOException e) { throw new RuntimeException(e); } responseType = determineResponseType(); } }