package org.jboss.as.test.integration.security.picketlink; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.text.StrSubstitutor; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; 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.params.ClientPNames; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.util.EntityUtils; import org.jboss.as.arquillian.api.ServerSetupTask; import org.jboss.as.network.NetworkUtils; import org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask; import org.jboss.as.test.integration.security.common.Krb5LoginConfiguration; import org.jboss.as.test.integration.security.common.Utils; import org.jboss.as.test.integration.security.common.config.SecurityDomain; import org.jboss.as.test.integration.security.common.config.SecurityModule; import org.jboss.logging.Logger; /** * Base class with common utilities for PicketLink integration tests * * @author Filip Bogyai */ class PicketLinkTestBase { public static final String ANIL = "anil"; public static final String MARCUS = "marcus"; public static final String USERS = ANIL + "=" + ANIL + "\n" + MARCUS + "=" + MARCUS; public static final String ROLES = ANIL + "=" + "gooduser" + "\n" + MARCUS + "=baduser"; private static final Logger LOGGER = Logger.getLogger(PicketLinkTestBase.class); /** * Requests given URL and checks if the returned HTTP status code is the expected one. Returns HTTP response body * * @param url url to which the request should be made * @param httpClient httpClient to test multiple access * @param expectedStatusCode expected status code returned from the requested server * @return HTTP response body * @throws ClientProtocolException * @throws IOException * @throws URISyntaxException */ public static String makeCall(URL url, HttpClient httpClient, int expectedStatusCode) throws IOException, URISyntaxException { String httpResponseBody = null; final URI requestURI = Utils.replaceHost(url.toURI(), Utils.getDefaultHost(true)); HttpGet httpGet = new HttpGet(requestURI); HttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); LOGGER.trace("Request to: " + requestURI + " responds: " + statusCode); assertEquals("Unexpected status code", expectedStatusCode, statusCode); HttpEntity entity = response.getEntity(); if (entity != null) { httpResponseBody = EntityUtils.toString(response.getEntity()); EntityUtils.consume(entity); } return httpResponseBody; } /** * Requests given URL and returns redirect location URL from response header. If response is not redirected then returns the * same URL which was requested * * @param url url to which the request should be made * @param httpClient httpClient to test multiple access * @return URL redirect location * @throws ClientProtocolException * @throws IOException * @throws URISyntaxException */ public static URL makeCallWithoutRedirect(URL url, HttpClient httpClient) throws IOException, URISyntaxException { HttpParams params = new BasicHttpParams(); params.setParameter(ClientPNames.HANDLE_REDIRECTS, false); String redirectLocation = url.toExternalForm(); final URI requestURI = Utils.replaceHost(url.toURI(), Utils.getDefaultHost(true)); HttpGet httpGet = new HttpGet(requestURI); httpGet.setParams(params); HttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); LOGGER.trace("Request to: " + requestURI + " responds: " + statusCode); Header locationHeader = response.getFirstHeader("location"); if (locationHeader != null) { redirectLocation = locationHeader.getValue(); } HttpEntity entity = response.getEntity(); if (entity != null) { EntityUtils.consume(entity); } return new URL(redirectLocation); } /** * Requests given SP and post SAMLRequest to IdP, then post back SAMLResponse. Returns HTTP response body * * @param spURL spURL of requested Service Provider * @param idpURL idpURL of Identity Provider * @param httpClient httpClient to test multiple access * @return HTTP response body * @throws ClientProtocolException * @throws IOException * @throws URISyntaxException */ public static String postSAML2Assertions(URL spURL, URL idpURL, HttpClient httpClient) throws IOException, URISyntaxException { final String canonicalHost = Utils.getDefaultHost(true); String httpResponseBody = makeCall(spURL, httpClient, 200); // parse SAMLRequest and post it to IdP String[] splitted = httpResponseBody.split("NAME=\"SAMLRequest\" VALUE=\""); String samlRequest = splitted[1].substring(0, splitted[1].indexOf("\"")); List<NameValuePair> pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("SAMLRequest", samlRequest)); HttpPost httpPost = new HttpPost(Utils.replaceHost(idpURL.toURI(), canonicalHost)); httpPost.setEntity(new UrlEncodedFormEntity(pairs)); HttpResponse httpResponse = httpClient.execute(httpPost); HttpEntity entity = httpResponse.getEntity(); if (entity != null) { httpResponseBody = EntityUtils.toString(httpResponse.getEntity()); EntityUtils.consume(entity); } // parse SAMLResponse and post it back to SP splitted = httpResponseBody.split("NAME=\"SAMLResponse\" VALUE=\""); String samlResponse = splitted[1].substring(0, splitted[1].indexOf("\"")); pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("SAMLResponse", samlResponse)); httpPost = new HttpPost(Utils.replaceHost(spURL.toURI(), canonicalHost)); httpPost.setEntity(new UrlEncodedFormEntity(pairs)); httpResponse = httpClient.execute(httpPost); entity = httpResponse.getEntity(); if (entity != null) { httpResponseBody = EntityUtils.toString(httpResponse.getEntity()); EntityUtils.consume(entity); } return httpResponseBody; } /** * Replace variables in PicketLink configurations files with given values and set ${hostname} variable from system property: * node0 * * @param resourceFile * @param deploymentName * @param bindingType * @return String content */ public static String propertiesReplacer(String resourceFile, String deploymentName, String bindingType, String idpContextPath) { final Map<String, String> map = new HashMap<String, String>(); String content = ""; map.put("hostname", NetworkUtils.formatPossibleIpv6Address(Utils.getDefaultHost(true))); map.put("deployment", deploymentName); map.put("bindingType", bindingType); map.put("idpContextPath", idpContextPath); try { content = StrSubstitutor.replace( IOUtils.toString(SAML2BasicAuthenticationTestCase.class.getResourceAsStream(resourceFile), "UTF-8"), map); } catch (IOException ex) { String message = "Cannot find or modify configuration file " + resourceFile + " , error : " + ex.getMessage(); LOGGER.error(message); throw new RuntimeException(ex); } return content; } /** * Returns response body for the given URL request as a String. It also checks if the returned HTTP status code is the * expected one. If the server returns {@link HttpServletResponse#SC_UNAUTHORIZED} and an username is provided, then the * given user is authenticated against Kerberos and a new request is executed under the new subject. * * @param uri URI to which the request should be made * @param user Username * @param pass Password * @param expectedStatusCode expected status code returned from the requested server * @return HTTP response body * @throws IOException * @throws URISyntaxException * @throws PrivilegedActionException * @throws LoginException */ public static String makeCallWithKerberosAuthn(URI uri, final HttpClient httpClient, final String user, final String pass, final int expectedStatusCode) throws IOException, URISyntaxException, PrivilegedActionException, LoginException { uri = Utils.replaceHost(uri, Utils.getDefaultHost(true)); LOGGER.trace("Requesting URI: " + uri); final HttpGet httpGet = new HttpGet(uri); final HttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); if (HttpServletResponse.SC_UNAUTHORIZED != statusCode || StringUtils.isEmpty(user)) { assertEquals("Unexpected HTTP response status code.", expectedStatusCode, statusCode); return EntityUtils.toString(response.getEntity()); } final HttpEntity entity = response.getEntity(); final Header[] authnHeaders = response.getHeaders("WWW-Authenticate"); assertTrue("WWW-Authenticate header is present", authnHeaders != null && authnHeaders.length > 0); final Set<String> authnHeaderValues = new HashSet<String>(); for (final Header header : authnHeaders) { authnHeaderValues.add(header.getValue()); } assertTrue("WWW-Authenticate: Negotiate header is missing", authnHeaderValues.contains("Negotiate")); if (LOGGER.isDebugEnabled()) { LOGGER.debug("HTTP response was SC_UNAUTHORIZED, let's authenticate the user " + user); } if (entity != null) { EntityUtils.consume(entity); } // Use our custom configuration to avoid reliance on external config final Krb5LoginConfiguration krb5configuration = new Krb5LoginConfiguration(Utils.getLoginConfiguration()); Configuration.setConfiguration(krb5configuration); // 1. Authenticate to Kerberos. final LoginContext lc = Utils.loginWithKerberos(krb5configuration, user, pass); // 2. Perform the work as authenticated Subject. final String responseBody = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<String>() { public String run() throws Exception { final HttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); assertEquals("Unexpected status code returned after the authentication.", expectedStatusCode, statusCode); return EntityUtils.toString(response.getEntity()); } }); lc.logout(); krb5configuration.resetConfiguration(); return responseBody; } /** * A {@link ServerSetupTask} instance which creates security domains for Identity Provider(IdP) and Service Provider(SP) * * @author Filip Bogyai */ static class SecurityDomainsSetup extends AbstractSecurityDomainsServerSetupTask { /** * Returns SecurityDomains configuration for this testcase. * * @see org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask#getSecurityDomains() */ @Override protected SecurityDomain[] getSecurityDomains() { final SecurityDomain idp = new SecurityDomain.Builder() .name("idp") .cacheType("default") .loginModules( new SecurityModule.Builder().name("UsersRoles").flag("required") .putOption("usersProperties", "users.properties") .putOption("rolesProperties", "roles.properties").build()) // .build(); final SecurityDomain sp = new SecurityDomain.Builder() .name("sp") .cacheType("default") .loginModules( new SecurityModule.Builder() .name("org.picketlink.identity.federation.bindings.jboss.auth.SAML2LoginModule") .flag("required").build()) // .build(); return new SecurityDomain[]{idp, sp}; } } }