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};
}
}
}