Java tutorial
/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat, Inc., 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.jboss.as.test.integration.security.common; import static org.jboss.as.test.integration.security.common.negotiation.KerberosTestUtils.OID_KERBEROS_V5; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.net.UnknownHostException; import java.security.MessageDigest; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; 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.SystemUtils; import org.apache.commons.lang.text.StrSubstitutor; import org.apache.directory.server.annotations.CreateTransport; import org.apache.http.Consts; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.ProtocolException; import org.apache.http.StatusLine; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpClient; import org.apache.http.client.RedirectStrategy; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.RequestConfig; 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.protocol.HttpClientContext; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.auth.BasicSchemeFactory; import org.apache.http.impl.auth.DigestSchemeFactory; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultRedirectStrategy; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.BasicHttpClientConnectionManager; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.jboss.as.arquillian.container.ManagementClient; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.OperationBuilder; import org.jboss.as.network.NetworkUtils; import org.jboss.as.test.integration.security.common.negotiation.JBossNegotiateSchemeFactory; import org.jboss.as.test.shared.TestSuiteEnvironment; import org.jboss.dmr.ModelNode; import org.jboss.logging.Logger; import org.jboss.security.auth.callback.UsernamePasswordHandler; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.asset.Asset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.exporter.ZipExporter; /** * Common utilities for JBoss AS security tests. * * @author Jan Lanik * @author Josef Cacek */ public class Utils extends CoreUtils { private static final Logger LOGGER = Logger.getLogger(Utils.class); public static final String UTF_8 = "UTF-8"; public static final boolean IBM_JDK = StringUtils.startsWith(SystemUtils.JAVA_VENDOR, "IBM"); public static final boolean OPEN_JDK = StringUtils.startsWith(SystemUtils.JAVA_VM_NAME, "OpenJDK"); public static final boolean ORACLE_JDK = StringUtils.startsWith(SystemUtils.JAVA_VM_NAME, "Java HotSpot"); /** The REDIRECT_STRATEGY for Apache HTTP Client */ public static final RedirectStrategy REDIRECT_STRATEGY = new DefaultRedirectStrategy() { @Override public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) { boolean isRedirect = false; try { isRedirect = super.isRedirected(request, response, context); } catch (ProtocolException e) { e.printStackTrace(); } if (!isRedirect) { final int responseCode = response.getStatusLine().getStatusCode(); isRedirect = (responseCode == 301 || responseCode == 302); } return isRedirect; } }; /** * Return MD5 hash of the given string value, encoded with given {@link Coding}. If the value or coding is <code>null</code> * then original value is returned. * * @param value * @param coding * @return encoded MD5 hash of the string or original value if some of parameters is null */ public static String hashMD5(String value, Coding coding) { return (coding == null || value == null) ? value : hash(value, "MD5", coding); } public static String hash(String target, String algorithm, Coding coding) { MessageDigest md = null; try { md = MessageDigest.getInstance(algorithm); } catch (Exception e) { e.printStackTrace(); } byte[] bytes = target.getBytes(); byte[] byteHash = md.digest(bytes); String encodedHash = null; switch (coding) { case BASE_64: encodedHash = Base64.getEncoder().encodeToString(byteHash); break; case HEX: encodedHash = toHex(byteHash); break; default: throw new IllegalArgumentException("Unsuported coding:" + coding.name()); } return encodedHash; } public static String toHex(byte[] bytes) { StringBuffer sb = new StringBuffer(bytes.length * 2); for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; // top 4 bits char c = (char) ((b >> 4) & 0xf); if (c > 9) c = (char) ((c - 10) + 'a'); else c = (char) (c + '0'); sb.append(c); // bottom 4 bits c = (char) (b & 0xf); if (c > 9) c = (char) ((c - 10) + 'a'); else c = (char) (c + '0'); sb.append(c); } return sb.toString(); } public static URL getResource(String name) { ClassLoader tccl = Thread.currentThread().getContextClassLoader(); return tccl.getResource(name); } private static final long STOP_DELAY_DEFAULT = 0; /** * stops execution of the program indefinitely useful in testsuite debugging */ public static void stop() { stop(STOP_DELAY_DEFAULT); } /** * stop test execution for a given time interval useful for debugging * * @param delay interval (milliseconds), if delay<=0, interval is considered to be infinite (Long.MAX_VALUE) */ public static void stop(long delay) { long currentTime = System.currentTimeMillis(); long remainingTime = 0 < delay ? currentTime + delay - System.currentTimeMillis() : Long.MAX_VALUE; while (remainingTime > 0) { try { Thread.sleep(remainingTime); } catch (InterruptedException ex) { remainingTime = currentTime + delay - System.currentTimeMillis(); continue; } } } public static void applyUpdates(final List<ModelNode> updates, final ModelControllerClient client) throws Exception { for (ModelNode update : updates) { applyUpdate(update, client); } } public static void applyUpdate(ModelNode update, final ModelControllerClient client) throws Exception { ModelNode result = client.execute(new OperationBuilder(update).build()); if (LOGGER.isInfoEnabled()) { LOGGER.trace("Client update: " + update); LOGGER.trace("Client update result: " + result); } if (result.hasDefined("outcome") && "success".equals(result.get("outcome").asString())) { LOGGER.debug("Operation succeeded."); } 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")); } } /** * Read the contents of an HttpResponse's entity and return it as a String. The content is converted using the character set * from the entity (if any), failing that, "ISO-8859-1" is used. * * @param response * @return * @throws IOException */ public static String getContent(HttpResponse response) throws IOException { return EntityUtils.toString(response.getEntity()); } /** * Makes HTTP call with FORM authentication. * * @param URL * @param user * @param pass * @param expectedStatusCode * @throws Exception */ public static void makeCall(String URL, String user, String pass, int expectedStatusCode) throws Exception { try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { HttpGet httpget = new HttpGet(URL); HttpResponse response = httpClient.execute(httpget); HttpEntity entity = response.getEntity(); if (entity != null) { EntityUtils.consume(entity); } // We should get the Login Page StatusLine statusLine = response.getStatusLine(); assertEquals(200, statusLine.getStatusCode()); // We should now login with the user name and password HttpPost httpost = new HttpPost(URL + "/j_security_check"); List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("j_username", user)); nvps.add(new BasicNameValuePair("j_password", pass)); httpost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8")); response = httpClient.execute(httpost); entity = response.getEntity(); if (entity != null) { EntityUtils.consume(entity); } statusLine = response.getStatusLine(); // Post authentication - we have a 302 assertEquals(302, statusLine.getStatusCode()); Header locationHeader = response.getFirstHeader("Location"); String location = locationHeader.getValue(); HttpGet httpGet = new HttpGet(location); response = httpClient.execute(httpGet); entity = response.getEntity(); if (entity != null) { EntityUtils.consume(entity); } // Either the authentication passed or failed based on the expected status code statusLine = response.getStatusLine(); assertEquals(expectedStatusCode, statusLine.getStatusCode()); } } /** * Exports given archive to the given file path. * * @param archive * @param filePath */ public static void saveArchive(Archive<?> archive, String filePath) { archive.as(ZipExporter.class).exportTo(new File(filePath), true); } /** * Exports given archive to the given folder. * * @param archive archive to export (not-<code>null</code>) * @param folderPath */ public static void saveArchiveToFolder(Archive<?> archive, String folderPath) { final File exportFile = new File(folderPath, archive.getName()); LOGGER.trace("Exporting archive: " + exportFile.getAbsolutePath()); archive.as(ZipExporter.class).exportTo(exportFile, true); } /** * Returns "secondary.test.address" system property if such exists. If not found, then there is a fallback to * {@link ManagementClient#getMgmtAddress()} or {@link #getDefaultHost(boolean)} (when mgmtClient is <code>null</code>). * Returned value can be converted to canonical hostname if useCanonicalHost==true. Returned value is not formatted for URLs * (i.e. square brackets are not placed around IPv6 addr - for instance "::1") * * @param mgmtClient management client instance (may be <code>null</code>) * @param useCanonicalHost * @return */ public static String getSecondaryTestAddress(final ManagementClient mgmtClient, final boolean useCanonicalHost) { String address = System.getProperty("secondary.test.address"); if (StringUtils.isBlank(address)) { address = mgmtClient != null ? mgmtClient.getMgmtAddress() : getDefaultHost(false); } if (useCanonicalHost) { address = getCannonicalHost(address); } return stripSquareBrackets(address); } /** * Returns "secondary.test.address" system property if such exists. If not found, then there is a fallback to * {@link ManagementClient#getMgmtAddress()}. Returned value is formatted to use in URLs (i.e. if it's IPv6 address, then * square brackets are placed around - e.g. "[::1]") * * @param mgmtClient management client instance (may be <code>null</code>) * @return */ public static String getSecondaryTestAddress(final ManagementClient mgmtClient) { return NetworkUtils.formatPossibleIpv6Address(getSecondaryTestAddress(mgmtClient, false)); } /** * 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 DefaultHttpClient to test multiple access * @param expectedStatusCode expected status code returned from the requested server * @return HTTP response body * @throws IOException * @throws URISyntaxException */ public static String makeCallWithHttpClient(URL url, HttpClient httpClient, int expectedStatusCode) throws IOException, URISyntaxException { String httpResponseBody = null; HttpGet httpGet = new HttpGet(url.toURI()); HttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); LOGGER.trace("Request to: " + url + " 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; } /** * 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 username is provided, then a new * request is created with the provided credentials (basic authentication). * * @param url URL to which the request should be made * @param user Username (may be null) * @param pass Password (may be null) * @param expectedStatusCode expected status code returned from the requested server * @return HTTP response body * @throws IOException * @throws URISyntaxException */ public static String makeCallWithBasicAuthn(URL url, String user, String pass, int expectedStatusCode) throws IOException, URISyntaxException { return makeCallWithBasicAuthn(url, user, pass, expectedStatusCode, false); } /** * 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 username is provided, then a new * request is created with the provided credentials (basic authentication). * * @param url URL to which the request should be made * @param user Username (may be null) * @param pass Password (may be null) * @param expectedStatusCode expected status code returned from the requested server * @param checkFollowupAuthState whether to check auth state for followup request - if set to true, followup * request is sent to server and 200 OK is expected directly (no re-authentication * challenge - 401 Unauthorized - is expected) * @return HTTP response body * @throws IOException * @throws URISyntaxException */ public static String makeCallWithBasicAuthn(URL url, String user, String pass, int expectedStatusCode, boolean checkFollowupAuthState) throws IOException, URISyntaxException { LOGGER.trace("Requesting URL " + url); // use UTF-8 charset for credentials Registry<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create() .register(AuthSchemes.BASIC, new BasicSchemeFactory(Consts.UTF_8)) .register(AuthSchemes.DIGEST, new DigestSchemeFactory(Consts.UTF_8)).build(); try (final CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultAuthSchemeRegistry(authSchemeRegistry).build()) { final HttpGet httpGet = new HttpGet(url.toURI()); 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()); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("HTTP response was SC_UNAUTHORIZED, let's authenticate the user " + user); } HttpEntity entity = response.getEntity(); if (entity != null) EntityUtils.consume(entity); final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(user, pass); HttpClientContext hc = new HttpClientContext(); hc.setCredentialsProvider(new BasicCredentialsProvider()); hc.getCredentialsProvider().setCredentials(new AuthScope(url.getHost(), url.getPort()), credentials); //enable auth response = httpClient.execute(httpGet, hc); statusCode = response.getStatusLine().getStatusCode(); assertEquals("Unexpected status code returned after the authentication.", expectedStatusCode, statusCode); if (checkFollowupAuthState) { // Let's disable authentication for this client as we already have all the context neccessary to be // authorized (we expect that gained 'nonce' value can be re-used in our case here). // By disabling authentication we simply get first server response and thus we can check whether we've // got 200 OK or different response code. RequestConfig reqConf = RequestConfig.custom().setAuthenticationEnabled(false).build(); httpGet.setConfig(reqConf); response = httpClient.execute(httpGet, hc); statusCode = response.getStatusLine().getStatusCode(); assertEquals("Unexpected status code returned after the authentication.", HttpURLConnection.HTTP_OK, statusCode); } return EntityUtils.toString(response.getEntity()); } } /** * 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(final URI uri, final String user, final String pass, final int expectedStatusCode) throws IOException, URISyntaxException, PrivilegedActionException, LoginException { LOGGER.trace("Requesting URI: " + uri); Registry<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create() .register(AuthSchemes.SPNEGO, new JBossNegotiateSchemeFactory(true)).build(); CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(new AuthScope(null, -1, null), new NullHCCredentials()); final Krb5LoginConfiguration krb5Configuration = new Krb5LoginConfiguration(getLoginConfiguration()); try (final CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultAuthSchemeRegistry(authSchemeRegistry).setDefaultCredentialsProvider(credentialsProvider) .build()) { 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 Configuration.setConfiguration(krb5Configuration); // 1. Authenticate to Kerberos. final LoginContext lc = 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(); return responseBody; } finally { krb5Configuration.resetConfiguration(); } } /** * Creates request against SPNEGO protected web-app with FORM fallback. It tries to login using SPNEGO first - if it fails, * FORM is used. * * @param contextUrl * @param page * @param user * @param pass * @param expectedStatusCode * @return * @throws IOException * @throws URISyntaxException * @throws PrivilegedActionException * @throws LoginException */ public static String makeHttpCallWithFallback(final String contextUrl, final String page, final String user, final String pass, final int expectedStatusCode) throws IOException, URISyntaxException, PrivilegedActionException, LoginException { final String strippedContextUrl = StringUtils.stripEnd(contextUrl, "/"); final String url = strippedContextUrl + page; LOGGER.trace("Requesting URL: " + url); String unauthorizedPageBody = null; final Krb5LoginConfiguration krb5Configuration = new Krb5LoginConfiguration(getLoginConfiguration()); Registry<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create() .register(AuthSchemes.SPNEGO, new JBossNegotiateSchemeFactory(true)).build(); CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(new AuthScope(null, -1, null), new NullHCCredentials()); final CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultAuthSchemeRegistry(authSchemeRegistry).setDefaultCredentialsProvider(credentialsProvider) .setRedirectStrategy(REDIRECT_STRATEGY).setConnectionManager(new BasicHttpClientConnectionManager()) .build(); try { final HttpGet httpGet = new HttpGet(url); 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 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")); LOGGER.debug("HTTP response was SC_UNAUTHORIZED, let's authenticate the user " + user); unauthorizedPageBody = EntityUtils.toString(response.getEntity()); // Use our custom configuration to avoid reliance on external config Configuration.setConfiguration(krb5Configuration); // 1. Authenticate to Kerberos. final LoginContext lc = 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(); return responseBody; } catch (LoginException e) { assertNotNull(unauthorizedPageBody); assertTrue(unauthorizedPageBody.contains("j_security_check")); HttpPost httpPost = new HttpPost(strippedContextUrl + "/j_security_check"); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(); nameValuePairs.add(new BasicNameValuePair("j_username", user)); nameValuePairs.add(new BasicNameValuePair("j_password", pass)); httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs)); final HttpResponse response = httpClient.execute(httpPost); int statusCode = response.getStatusLine().getStatusCode(); assertEquals("Unexpected status code returned after the authentication.", expectedStatusCode, statusCode); return EntityUtils.toString(response.getEntity()); } finally { // When HttpClient instance is no longer needed, // shut down the connection manager to ensure // immediate deallocation of all system resources httpClient.close(); // reset login configuration krb5Configuration.resetConfiguration(); } } /** * Creates request against SPNEGO protected web-app with FORM fallback. It doesn't try to login using SPNEGO - it uses FORM * authn directly. * * @param contextUrl * @param page * @param user * @param pass * @param expectedStatusCode * @return * @throws IOException * @throws URISyntaxException * @throws PrivilegedActionException * @throws LoginException */ public static String makeHttpCallWoSPNEGO(final String contextUrl, final String page, final String user, final String pass, final int expectedStatusCode) throws IOException, URISyntaxException, PrivilegedActionException, LoginException { final String strippedContextUrl = StringUtils.stripEnd(contextUrl, "/"); final String url = strippedContextUrl + page; LOGGER.trace("Requesting URL: " + url); String unauthorizedPageBody = null; try (final CloseableHttpClient httpClient = HttpClientBuilder.create() .setRedirectStrategy(REDIRECT_STRATEGY).build()) { final HttpGet httpGet = new HttpGet(url); 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 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")); LOGGER.debug("HTTP response was SC_UNAUTHORIZED, let's authenticate the user " + user); unauthorizedPageBody = EntityUtils.toString(response.getEntity()); assertNotNull(unauthorizedPageBody); LOGGER.trace(unauthorizedPageBody); assertTrue(unauthorizedPageBody.contains("j_security_check")); HttpPost httpPost = new HttpPost(strippedContextUrl + "/j_security_check"); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(); nameValuePairs.add(new BasicNameValuePair("j_username", user)); nameValuePairs.add(new BasicNameValuePair("j_password", pass)); httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs)); response = httpClient.execute(httpPost); statusCode = response.getStatusLine().getStatusCode(); assertEquals("Unexpected status code returned after the authentication.", expectedStatusCode, statusCode); return EntityUtils.toString(response.getEntity()); } } /** * Sets or removes (in case value==null) a system property. It's only a helper method, which avoids * {@link NullPointerException} thrown from {@link System#setProperty(String, String)} method, when the value is * <code>null</code>. * * @param key property name * @param value property value * @return the previous string value of the system property */ public static String setSystemProperty(final String key, final String value) { return value == null ? System.clearProperty(key) : System.setProperty(key, value); } /** * Generates content of jboss-ejb3.xml file as a ShrinkWrap asset with the given security domain name. * * @param securityDomain security domain name * @return Asset instance */ public static Asset getJBossEjb3XmlAsset(final String securityDomain) { final StringBuilder sb = new StringBuilder(); sb.append("<jboss:ejb-jar xmlns:jboss='http://www.jboss.com/xml/ns/javaee'"); sb.append("\n\txmlns='http://java.sun.com/xml/ns/javaee'"); sb.append("\n\txmlns:s='urn:security'"); sb.append("\n\tversion='3.1'"); sb.append("\n\timpl-version='2.0'>"); sb.append("\n\t<assembly-descriptor><s:security>"); sb.append("\n\t\t<ejb-name>*</ejb-name>"); sb.append("\n\t\t<s:security-domain>").append(securityDomain).append("</s:security-domain>"); sb.append("\n\t</s:security></assembly-descriptor>"); sb.append("\n</jboss:ejb-jar>"); return new StringAsset(sb.toString()); } /** * Generates content of jboss-web.xml file as a ShrinkWrap asset with the given security domain name and given valve class. * * @param securityDomain security domain name (not-<code>null</code>) * @param valveClassNames valve class (e.g. an Authenticator) which should be added to jboss-web file (may be * <code>null</code>) * @return Asset instance */ public static Asset getJBossWebXmlAsset(final String securityDomain, final String... valveClassNames) { final StringBuilder sb = new StringBuilder(); sb.append("<jboss-web>"); sb.append("\n\t<security-domain>").append(securityDomain).append("</security-domain>"); if (valveClassNames != null) { for (String valveClassName : valveClassNames) { if (StringUtils.isNotEmpty(valveClassName)) { sb.append("\n\t<valve><class-name>").append(valveClassName).append("</class-name></valve>"); } } } sb.append("\n</jboss-web>"); return new StringAsset(sb.toString()); } /** * Generates content of the jboss-deployment-structure.xml deployment descriptor as a ShrinkWrap asset. It fills the given * dependencies (module names) into it. * * @param dependencies AS module names * @return */ public static Asset getJBossDeploymentStructure(String... dependencies) { final StringBuilder sb = new StringBuilder(); sb.append("<jboss-deployment-structure><deployment><dependencies>"); if (dependencies != null) { for (String moduleName : dependencies) { sb.append("\n\t<module name='").append(moduleName).append("'/>"); } } sb.append("\n</dependencies></deployment></jboss-deployment-structure>"); return new StringAsset(sb.toString()); } /** * Creates content of users.properties and/or roles.properties files for given array of role names. * <p> * For instance if you provide 2 roles - "role1", "role2" then the result will be: * * <pre> * role1=role1 * role2=role2 * </pre> * * If you use it as users.properties and roles.properties, then <code>roleName == userName == password</code> * * @param roles role names (used also as user names and passwords) * @return not-<code>null</code> content of users.properties and/or roles.properties */ public static String createUsersFromRoles(String... roles) { final StringBuilder sb = new StringBuilder(); if (roles != null) { for (String role : roles) { sb.append(role).append("=").append(role).append("\n"); } } return sb.toString(); } /** * Strips square brackets - '[' and ']' from the given string. It can be used for instance to remove the square brackets * around IPv6 address in a URL. * * @param str string to strip * @return str without square brackets in it */ public static String stripSquareBrackets(final String str) { return StringUtils.strip(str, "[]"); } /** * Fixes/replaces LDAP bind address in the CreateTransport annotation of ApacheDS. * * @param createLdapServer * @param address */ public static void fixApacheDSTransportAddress(ManagedCreateLdapServer createLdapServer, String address) { final CreateTransport[] createTransports = createLdapServer.transports(); for (int i = 0; i < createTransports.length; i++) { final ManagedCreateTransport mgCreateTransport = new ManagedCreateTransport(createTransports[i]); // localhost is a default used in original CreateTransport annotation. We use it as a fallback. mgCreateTransport.setAddress(address != null ? address : "localhost"); createTransports[i] = mgCreateTransport; } } /** * Copies server and clients keystores and truststores from this package to the given folder. Server truststore has accepted * certificate from client keystore and vice-versa * * @param workingFolder folder to which key material should be copied * @throws IOException copying of keystores fails * @throws IllegalArgumentException workingFolder is null or it's not a directory */ public static void createKeyMaterial(final File workingFolder) throws IOException, IllegalArgumentException { if (workingFolder == null || !workingFolder.isDirectory()) { throw new IllegalArgumentException("Provide an existing folder as the method parameter."); } createTestResource(new File(workingFolder, SecurityTestConstants.SERVER_KEYSTORE)); createTestResource(new File(workingFolder, SecurityTestConstants.SERVER_TRUSTSTORE)); createTestResource(new File(workingFolder, SecurityTestConstants.SERVER_CRT)); createTestResource(new File(workingFolder, SecurityTestConstants.CLIENT_KEYSTORE)); createTestResource(new File(workingFolder, SecurityTestConstants.CLIENT_TRUSTSTORE)); createTestResource(new File(workingFolder, SecurityTestConstants.CLIENT_CRT)); createTestResource(new File(workingFolder, SecurityTestConstants.UNTRUSTED_KEYSTORE)); createTestResource(new File(workingFolder, SecurityTestConstants.UNTRUSTED_CRT)); LOGGER.trace("Key material created in " + workingFolder.getAbsolutePath()); } /** * Copies a resource file from current package to location denoted by given {@link File} instance. * * @param file * * @throws IOException */ private static void createTestResource(File file) throws IOException { FileOutputStream fos = null; LOGGER.trace("Creating test file " + file.getAbsolutePath()); try { fos = new FileOutputStream(file); IOUtils.copy(CoreUtils.class.getResourceAsStream(file.getName()), fos); } finally { IOUtils.closeQuietly(fos); } } public static String propertiesReplacer(String originalFile, File keystoreFile, File trustStoreFile, String keystorePassword) { return propertiesReplacer(originalFile, keystoreFile.getAbsolutePath(), trustStoreFile.getAbsolutePath(), keystorePassword, null); } public static String propertiesReplacer(String originalFile, File keystoreFile, File trustStoreFile, String keystorePassword, String vaultConfig) { return propertiesReplacer(originalFile, keystoreFile.getAbsolutePath(), trustStoreFile.getAbsolutePath(), keystorePassword, vaultConfig); } /** * Replace keystore paths and passwords variables in original configuration file with given values and set ${hostname} * variable from system property: node0 * * @param originalFile String * @param keystoreFile File * @param trustStoreFile File * @param keystorePassword String * @param vaultConfig - path to vault settings * @return String content */ public static String propertiesReplacer(String originalFile, String keystoreFile, String trustStoreFile, String keystorePassword, String vaultConfig) { String hostname = getDefaultHost(false); // expand possible IPv6 address try { hostname = NetworkUtils.formatPossibleIpv6Address(InetAddress.getByName(hostname).getHostAddress()); } catch (UnknownHostException ex) { String message = "Cannot resolve host address: " + hostname + " , error : " + ex.getMessage(); LOGGER.error(message); throw new RuntimeException(ex); } final Map<String, String> map = new HashMap<String, String>(); String content = ""; if (vaultConfig == null) { map.put("vaultConfig", ""); } else { map.put("vaultConfig", vaultConfig); } map.put("hostname", hostname); map.put("keystore", keystoreFile); map.put("truststore", trustStoreFile); map.put("password", keystorePassword); try { content = StrSubstitutor .replace(IOUtils.toString(CoreUtils.class.getResourceAsStream(originalFile), "UTF-8"), map); } catch (IOException ex) { String message = "Cannot find or modify configuration file " + originalFile + " , error : " + ex.getMessage(); LOGGER.error(message); throw new RuntimeException(ex); } return content; } /** * Makes HTTP call without authentication. Returns response body as a String. * * @param uri requested URL * @param expectedStatusCode expected status code - it's checked after the request is executed * @throws Exception */ public static String makeCall(URI uri, int expectedStatusCode) throws Exception { try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { final HttpGet httpget = new HttpGet(uri); final HttpResponse response = httpClient.execute(httpget); int statusCode = response.getStatusLine().getStatusCode(); assertEquals("Unexpected status code in HTTP response.", expectedStatusCode, statusCode); return EntityUtils.toString(response.getEntity()); } } /** * Returns param/value pair in form "urlEncodedName=urlEncodedValue". It can be used for instance in HTTP get queries. * * @param paramName parameter name * @param paramValue parameter value * @return "[urlEncodedName]=[urlEncodedValue]" string */ public static String encodeQueryParam(final String paramName, final String paramValue) { String response = null; try { response = StringUtils.isEmpty(paramValue) ? null : (URLEncoder.encode(paramName, UTF_8) + "=" + URLEncoder.encode(StringUtils.defaultString(paramValue, StringUtils.EMPTY), UTF_8)); } catch (UnsupportedEncodingException e) { // should never happen - everybody likes the "UTF-8" :) } return response; } /** * Returns management address (host) from the givem {@link org.jboss.as.arquillian.container.ManagementClient}. If the * returned value is IPv6 address then square brackets around are stripped. * * @param managementClient * @return */ public static final String getHost(final ManagementClient managementClient) { return CoreUtils.stripSquareBrackets(managementClient.getMgmtAddress()); } /** * Returns canonical hostname retrieved from management address of the givem * {@link org.jboss.as.arquillian.container.ManagementClient}. * * @param managementClient * @return */ public static final String getCannonicalHost(final ManagementClient managementClient) { return getCannonicalHost(managementClient.getMgmtAddress()); } /** * Returns servlet URL, as concatenation of webapp URL and servlet path. * * @param webAppURL web application context URL (e.g. injected by Arquillian) * @param servletPath Servlet path starting with slash (must be not-<code>null</code>) * @param mgmtClient Management Client (may be null) * @param useCanonicalHost flag which says if host in URI should be replaced by the canonical host. * @return * @throws java.net.URISyntaxException */ public static final URI getServletURI(final URL webAppURL, final String servletPath, final ManagementClient mgmtClient, boolean useCanonicalHost) throws URISyntaxException { URI resultURI = new URI(webAppURL.toExternalForm() + servletPath.substring(1)); if (useCanonicalHost) { resultURI = replaceHost(resultURI, getCannonicalHost(mgmtClient)); } return resultURI; } /** * Returns hostname - either read from the "node0" system property or the loopback address "127.0.0.1". * * @param canonical return hostname in canonical form * * @return */ public static String getDefaultHost(boolean canonical) { final String hostname = TestSuiteEnvironment.getHttpAddress(); return canonical ? getCannonicalHost(hostname) : hostname; } /** * Returns installed login configuration. * * @return Configuration */ public static Configuration getLoginConfiguration() { Configuration configuration = null; try { configuration = Configuration.getConfiguration(); } catch (SecurityException e) { LOGGER.debug("Unable to load default login configuration", e); } return configuration; } /** * Creates login context for given {@link Krb5LoginConfiguration} and credentials and calls the {@link LoginContext#login()} * method on it. This method contains workaround for IBM JDK issue described in bugzilla <a * href="https://bugzilla.redhat.com/show_bug.cgi?id=1206177">https://bugzilla.redhat.com/show_bug.cgi?id=1206177</a>. * * @param krb5Configuration * @param user * @param pass * @return * @throws LoginException */ public static LoginContext loginWithKerberos(final Krb5LoginConfiguration krb5Configuration, final String user, final String pass) throws LoginException { LoginContext lc = new LoginContext(krb5Configuration.getName(), new UsernamePasswordHandler(user, pass)); if (IBM_JDK) { // workaround for IBM JDK on RHEL5 issue described in https://bugzilla.redhat.com/show_bug.cgi?id=1206177 // The first negotiation always fail, so let's do a dummy login/logout round. lc.login(); lc.logout(); lc = new LoginContext(krb5Configuration.getName(), new UsernamePasswordHandler(user, pass)); } lc.login(); return lc; } /** * Creates Kerberos TGS ticket for given user to access given server. * * @param user * @param pass * @param serverName * @return */ public static byte[] createKerberosTicketForServer(final String user, final String pass, final GSSName serverName) throws MalformedURLException, LoginException, PrivilegedActionException { Objects.requireNonNull(serverName); final Krb5LoginConfiguration krb5Configuration = new Krb5LoginConfiguration(getLoginConfiguration()); try { Configuration.setConfiguration(krb5Configuration); final LoginContext lc = loginWithKerberos(krb5Configuration, user, pass); try { return Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<byte[]>() { public byte[] run() throws Exception { final GSSManager manager = GSSManager.getInstance(); final Oid oid = new Oid(OID_KERBEROS_V5); final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, null, 60); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); return gssContext.initSecContext(new byte[0], 0, 0); } }); } finally { lc.logout(); } } finally { krb5Configuration.resetConfiguration(); } } /** * Asserts that the given HttpResponse contains header with given name and value. * * @param resp HttpResponse (from Apache HttpClient) * @param headerName name of HTTP header * @param expectedVal expected HTTP header value */ public static void assertHttpHeader(HttpResponse resp, String headerName, String expectedVal) { final Header[] authnHeaders = resp.getHeaders(headerName); assertTrue("Header " + headerName + " should be present in the HTTP response", authnHeaders != null && authnHeaders.length > 0); for (final Header header : authnHeaders) { if (expectedVal.equals(header.getValue())) { return; } } fail("HTTP Header not found '" + headerName + ": " + expectedVal + "'"); } /** * Creates a temporary folder name with given name prefix. * * @param prefix folder name prefix * @return created folder */ public static File createTemporaryFolder(String prefix) throws IOException { File file = File.createTempFile(prefix, "", null); LOGGER.debugv("Creating temporary folder {0}", file); file.delete(); file.mkdir(); return file; } }