/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, 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.loginmodules.negotiation;
import static org.jboss.as.test.integration.security.common.Utils.assertHttpHeader;
import static org.jboss.as.test.integration.security.common.negotiation.KerberosTestUtils.OID_DUMMY;
import static org.jboss.as.test.integration.security.common.negotiation.KerberosTestUtils.OID_KERBEROS_V5;
import static org.jboss.as.test.integration.security.common.negotiation.KerberosTestUtils.OID_KERBEROS_V5_LEGACY;
import static org.jboss.as.test.integration.security.common.negotiation.KerberosTestUtils.OID_NTLM;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketPermission;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.PrivilegedActionException;
import java.security.Security;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.PropertyPermission;
import javax.security.auth.kerberos.ServicePermission;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.server.annotations.CreateKdcServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.core.annotations.ContextEntry;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.annotations.CreateIndex;
import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.factory.DSAnnotationProcessor;
import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
import org.apache.directory.server.kerberos.kdc.KdcServer;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
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.arquillian.test.api.ArquillianResource;
import org.jboss.as.arquillian.api.ServerSetup;
import org.jboss.as.arquillian.api.ServerSetupTask;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.network.NetworkUtils;
import org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask;
import org.jboss.as.test.integration.security.common.AbstractSystemPropertiesServerSetupTask;
import org.jboss.as.test.integration.security.common.KDCServerAnnotationProcessor;
import org.jboss.as.test.integration.security.common.SecurityTraceLoggingServerSetupTask;
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.as.test.integration.security.common.negotiation.KerberosTestUtils;
import org.jboss.as.test.integration.security.common.servlets.SimpleSecuredServlet;
import org.jboss.as.test.integration.security.common.servlets.SimpleServlet;
import org.jboss.as.test.integration.security.loginmodules.LdapExtLoginModuleTestCase;
import org.jboss.as.test.shared.TestSuiteEnvironment;
import org.jboss.as.test.shared.integration.ejb.security.PermissionUtils;
import org.jboss.logging.Logger;
import org.jboss.security.SecurityConstants;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Basic Negotiation login module (SPNEGOLoginModule) tests.
* <p>
* Some of the tests check the negotiation workflow if it fits the following RFCs:
* </p>
* <ul>
* <li><a href="https://tools.ietf.org/html/rfc2743">RFC-2743 - GSS-API</a></li>
* <li><a href="https://tools.ietf.org/html/rfc4120">RFC-4120 The Kerberos Network Authentication Service (V5)</a></li>
* <li><a href="https://tools.ietf.org/html/rfc4121">RFC-4121 The Kerberos Version 5 GSS-API</a></li>
* <li><a href="https://tools.ietf.org/html/rfc4178">RFC-4178 SPNEGO</a></li>
* <li><a href="https://tools.ietf.org/html/rfc4559">RFC-4559 SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft
* Windows</a></li>
* </ul>
*
* @author Josef Cacek
*/
@RunWith(Arquillian.class)
@ServerSetup({SecurityTraceLoggingServerSetupTask.class, Krb5ConfServerSetupTask.class, //
SPNEGOLoginModuleTestCase.KerberosSystemPropertiesSetupTask.class, //
SPNEGOLoginModuleTestCase.KDCServerSetupTask.class, //
GSSTestServer.class, //
SPNEGOLoginModuleTestCase.SecurityDomainsSetup.class})
@RunAsClient
public class SPNEGOLoginModuleTestCase {
private static Logger LOGGER = Logger.getLogger(SPNEGOLoginModuleTestCase.class);
/**
* The WEBAPP_NAME
*/
private static final String WEBAPP_NAME = "kerberos-login-module";
private static final String WEBAPP_NAME_FALLBACK = "kerberos-test-form-fallback";
private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
private static final String HEADER_AUTHORIZATION = "Authorization";
private static final String HEADER_VAL_SELECT_KERBEROS_MECH = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg==";
private static final byte[] DUMMY_TOKEN = "Ahoj, svete!".getBytes(StandardCharsets.UTF_8);
/**
* The TRUE
*/
private static final String TRUE = Boolean.TRUE.toString();
@ArquillianResource
ManagementClient mgmtClient;
// Public methods --------------------------------------------------------
@Before
public void before() {
KerberosTestUtils.assumeKerberosAuthenticationSupported();
}
/**
* Creates {@link WebArchive}.
*
* @return
*/
@Deployment(name = "WEB", testable = false)
public static WebArchive deployment() {
LOGGER.debug("Web deployment");
final WebArchive war = createWebApp(WEBAPP_NAME, "web-spnego-authn.xml", "SPNEGO");
war.addAsManifestResource(PermissionUtils.createPermissionsXmlAsset(
// Permissions for PropagateIdentityServlet to get delegation credentials DelegationCredentialContext.getDelegCredential()
new RuntimePermission("org.jboss.security.negotiation.getDelegCredential"),
// Permissions for PropagateIdentityServlet to read properties
new PropertyPermission(GSSTestConstants.PROPERTY_PORT, "read"),
new PropertyPermission(GSSTestConstants.PROPERTY_PRINCIPAL, "read"),
new PropertyPermission(GSSTestConstants.PROPERTY_PASSWORD, "read"),
// Permissions for GSSTestClient to connect to GSSTestServer
new SocketPermission(TestSuiteEnvironment.getServerAddress(), "resolve,connect"),
// Permissions for GSSTestClient to initiate gss context
new ServicePermission(GSSTestConstants.PRINCIPAL, "initiate"),
new ServicePermission("krbtgt/JBOSS.ORG@JBOSS.ORG", "initiate")),
"permissions.xml");
return war;
}
/**
* Creates {@link WebArchive}.
*
* @return
*/
@Deployment(name = "WEB-FORM", testable = false)
public static WebArchive deploymentWebFormFallback() {
LOGGER.debug("Web deployment with FORM fallback");
final WebArchive war = createWebApp(WEBAPP_NAME_FALLBACK, "web-spnego-form-fallback.xml", "SPNEGO-with-fallback");
war.addAsWebResource(LdapExtLoginModuleTestCase.class.getPackage(), "error.jsp", "error.jsp");
war.addAsWebResource(LdapExtLoginModuleTestCase.class.getPackage(), "login.jsp", "login.jsp");
war.addAsResource(new StringAsset("jduke@JBOSS.ORG=fallback\nhnelson@JBOSS.ORG=terces"), "fallback-users.properties");
war.addAsResource(EmptyAsset.INSTANCE, "fallback-roles.properties");
return war;
}
/**
* Correct login.
*
* @throws Exception
*/
@Test
@OperateOnDeployment("WEB")
public void testAuthn(@ArquillianResource URL webAppURL) throws Exception {
final URI servletUri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
LOGGER.trace("Testing successful authentication " + servletUri);
final String responseBody = Utils.makeCallWithKerberosAuthn(servletUri, "jduke", "theduke", HttpServletResponse.SC_OK);
assertEquals("Unexpected response body", SimpleSecuredServlet.RESPONSE_BODY, responseBody);
}
/**
* Incorrect login.
*
* @throws Exception
*/
@Test
@OperateOnDeployment("WEB")
public void testUnsuccessfulAuthn(@ArquillianResource URL webAppURL) throws Exception {
final URI servletUri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
LOGGER.trace("Testing failed authentication " + servletUri);
try {
Utils.makeCallWithKerberosAuthn(servletUri, "jduke", "the%", HttpServletResponse.SC_OK);
fail();
} catch (LoginException e) {
// OK
}
try {
Utils.makeCallWithKerberosAuthn(servletUri, "jd%", "theduke", HttpServletResponse.SC_OK);
fail();
} catch (LoginException e) {
// OK
}
}
/**
* Correct login, but without permissions.
*
* @throws Exception
*/
@Test
@OperateOnDeployment("WEB")
public void testUnsuccessfulAuthz(@ArquillianResource URL webAppURL) throws Exception {
final URI servletUri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
LOGGER.trace("Testing correct authentication, but failed authorization " + servletUri);
Utils.makeCallWithKerberosAuthn(servletUri, "hnelson", "secret", HttpServletResponse.SC_FORBIDDEN);
}
/**
* Unsecured request.
*
* @throws Exception
*/
@Test
@OperateOnDeployment("WEB")
public void testUnsecured(@ArquillianResource URL webAppURL) throws Exception {
final URI servletUri = getServletURI(webAppURL, SimpleServlet.SERVLET_PATH);
LOGGER.trace("Testing access to unprotected resource " + servletUri);
final String responseBody = Utils.makeCallWithKerberosAuthn(servletUri, null, null, HttpServletResponse.SC_OK);
assertEquals("Unexpected response body.", SimpleServlet.RESPONSE_BODY, responseBody);
}
/**
* Tests identity propagation by requesting {@link PropagateIdentityServlet}.
*
* @throws Exception
*/
@Test
@OperateOnDeployment("WEB")
public void testIdentityPropagation(@ArquillianResource URL webAppURL) throws Exception {
KerberosTestUtils.assumeKerberosAuthenticationSupported();
final URI servletUri = getServletURI(webAppURL, PropagateIdentityServlet.SERVLET_PATH);
LOGGER.trace("Testing identity propagation " + servletUri);
final String responseBody = Utils.makeCallWithKerberosAuthn(servletUri, "jduke", "theduke", HttpServletResponse.SC_OK);
assertEquals("Unexpected response body.", "jduke@JBOSS.ORG", responseBody);
}
/**
* Tests web SPNEGO authentication with FORM method fallback.
*
* @throws Exception
*/
@Test
@OperateOnDeployment("WEB-FORM")
public void testFormFallback(@ArquillianResource URL webAppURL) throws Exception {
KerberosTestUtils.assumeKerberosAuthenticationSupported();
final URI servletUri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
LOGGER.trace("Testing fallback to FORM authentication. " + servletUri);
LOGGER.trace("Testing successful SPNEGO authentication");
String responseBody = Utils.makeCallWithKerberosAuthn(servletUri, "jduke", "theduke", HttpServletResponse.SC_OK);
assertEquals("Unexpected response body", SimpleSecuredServlet.RESPONSE_BODY, responseBody);
LOGGER.trace("Testing successful FORM authentication");
responseBody = Utils.makeHttpCallWoSPNEGO(webAppURL.toExternalForm(), SimpleSecuredServlet.SERVLET_PATH,
"jduke@JBOSS.ORG", "fallback", HttpServletResponse.SC_OK);
assertEquals("Unexpected response body", SimpleSecuredServlet.RESPONSE_BODY, responseBody);
LOGGER.trace("Testing FORM fallback");
responseBody = Utils.makeHttpCallWithFallback(webAppURL.toExternalForm(), SimpleSecuredServlet.SERVLET_PATH,
"jduke@JBOSS.ORG", "fallback", HttpServletResponse.SC_OK);
assertEquals("Unexpected response body", SimpleSecuredServlet.RESPONSE_BODY, responseBody);
}
/**
* SPNEGO simple scenario - only kerberos mechanism is provided with valid token.
*/
@Test
@OperateOnDeployment("WEB")
public void testSimpleSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
KerberosTestUtils.assumeKerberosAuthenticationSupported();
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_KERBEROS_V5};
assertSpnegoWorkflow(uri, mechTypes, createNewKerberosTicketForHttp(uri), null, false, true);
}
/**
* SPNEGO simple scenario - more mechanismTypes is provided but the Kerberos mechanism is most preferable one and it has a
* valid token.
*/
@Test
@OperateOnDeployment("WEB")
public void testMoreMechTypesSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
KerberosTestUtils.assumeKerberosAuthenticationSupported();
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_KERBEROS_V5, OID_DUMMY, OID_KERBEROS_V5_LEGACY};
assertSpnegoWorkflow(uri, mechTypes, createNewKerberosTicketForHttp(uri), null, false, true);
}
/**
* SPNEGO continuation scenario - more mechanismTypes is provided and the Kerberos mechanism is not the most preferable one.
* Client provides valid token in the second round.
*/
@Test
@OperateOnDeployment("WEB")
public void testContSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_DUMMY, OID_KERBEROS_V5_LEGACY, OID_KERBEROS_V5};
assertSpnegoWorkflow(uri, mechTypes, DUMMY_TOKEN, createNewKerberosTicketForHttp(uri), true, true);
}
/**
* SPNEGO continuation scenario - Kerberos mechanisms are provided as mechanismTypes. The Legacy (aka Microsoft) mechanism
* is provided as the first one and we expect the server will not accept it and it'll ask the token for the standard
* Kerberos mechanism OID. Client provides valid token in both rounds.
*/
@Test
@OperateOnDeployment("WEB")
public void testLegacyKerberosSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_KERBEROS_V5_LEGACY, OID_KERBEROS_V5};
final byte[] kerberosToken = createNewKerberosTicketForHttp(uri);
assertSpnegoWorkflow(uri, mechTypes, kerberosToken, kerberosToken, false, true);
}
/**
* SPNEGO simple scenario - more mechanismTypes is provided but the Kerberos mechanism is not listed as the supported one.
*/
@Test
@OperateOnDeployment("WEB")
public void testNoKerberosSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_DUMMY, OID_NTLM};
assertSpnegoWorkflow(uri, mechTypes, DUMMY_TOKEN, null, false, false);
}
/**
* SPNEGO continuation scenario - more mechanismTypes is provided and the Kerberos mechanism is not the most preferable one.
* Client provides invalid token in the second round.
*/
@Test
@OperateOnDeployment("WEB")
public void testContInvalidKerberosSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_DUMMY, OID_KERBEROS_V5};
assertSpnegoWorkflow(uri, mechTypes, DUMMY_TOKEN, DUMMY_TOKEN, true, false);
}
/**
* SPNEGO simple scenario - more mechanismTypes is provided and the Kerberos mechanism is the most preferable one. Client
* provides invalid token in the first round.
*/
@Test
@OperateOnDeployment("WEB")
@Ignore("JBEAP-4114")
public void testInvalidKerberosSpnegoWorkflow(@ArquillianResource URL webAppURL) throws Exception {
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final String[] mechTypes = new String[]{OID_KERBEROS_V5, OID_DUMMY};
assertSpnegoWorkflow(uri, mechTypes, DUMMY_TOKEN, null, false, false);
}
/**
* Kerberos simple scenario. Client provides a valid Kerberos token (without SPNEGO envelope) in the first round. See
* <a href="https://tools.ietf.org/html/rfc4121">RFC-4121</a>.
*/
@Test
@OperateOnDeployment("WEB")
public void testPlainKerberosWorkflow(@ArquillianResource URL webAppURL) throws Exception {
final URI uri = getServletURI(webAppURL, SimpleSecuredServlet.SERVLET_PATH);
final byte[] kerberosToken = createNewKerberosTicketForHttp(uri);
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
final HttpGet httpGet = new HttpGet(uri);
HttpResponse response = httpClient.execute(httpGet);
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatusLine().getStatusCode());
assertHttpHeader(response, HEADER_WWW_AUTHENTICATE, "Negotiate");
EntityUtils.consume(response.getEntity());
httpGet.setHeader(HEADER_AUTHORIZATION, "Negotiate " + Base64.getEncoder().encodeToString(kerberosToken));
response = httpClient.execute(httpGet);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Negotiate response in HTTP header:\n" + KerberosTestUtils.dumpNegotiateHeader(response));
}
assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
assertEquals("Unexpected response body", SimpleSecuredServlet.RESPONSE_BODY,
EntityUtils.toString(response.getEntity()));
}
}
// Private methods -------------------------------------------------------
private static WebArchive createWebApp(final String webAppName, final String webXmlFilename, final String securityDomain) {
final WebArchive war = ShrinkWrap.create(WebArchive.class, webAppName + ".war");
war.addClasses(SimpleSecuredServlet.class, SimpleServlet.class, PropagateIdentityServlet.class, GSSTestClient.class,
GSSTestConstants.class);
war.addAsWebInfResource(SPNEGOLoginModuleTestCase.class.getPackage(), webXmlFilename, "web.xml");
war.addAsWebInfResource(Utils.getJBossWebXmlAsset(securityDomain), "jboss-web.xml");
war.addAsManifestResource(
Utils.getJBossDeploymentStructure("org.jboss.security.negotiation", "org.apache.commons.lang"),
"jboss-deployment-structure.xml");
return war;
}
/**
* Constructs URI for given servlet path.
*
* @param servletPath
* @return
* @throws URISyntaxException
*/
private URI getServletURI(final URL webAppURL, final String servletPath) throws URISyntaxException {
return Utils.getServletURI(webAppURL, servletPath, mgmtClient, true);
}
/**
* Create a new Kerberos ticket for HTTP service. The ticket should be newly generated for every test to avoid the
* "ticket reply errors".
*
* @param uri servlet URI (used to retrieve hostname)
* @return ASN.1 (DER) encoded Kerberos key for HTTP service
*/
private byte[] createNewKerberosTicketForHttp(URI uri)
throws GSSException, MalformedURLException, LoginException, PrivilegedActionException {
final GSSName serverName = GSSManager.getInstance().createName("HTTP@" + uri.getHost(), GSSName.NT_HOSTBASED_SERVICE);
return Utils.createKerberosTicketForServer("jduke", "theduke", serverName);
}
/**
* Implements testing of SPNEGO authentication workflow with configurable parameters such supported mechanisms, tokens,
* expected continuation and checking responses.
*
* @param uri test URI which is protected by SPNEGO/Kerberos authentication.
* @param mechTypesOids array of supported mechanisms by the client in decreasing preference order (favorite choice first)
* @param initMechToken initial token (optimistic mechanism token) - token for the first of supported mechanisms
* @param responseToken token which is used in the second round when the server requests using Kerberos as a mechanism for
* authentication
* @param continuationExpected flag which says that we expect server to require the second round of authentication (i.e.
* server asks to send Kerberos token)
* @param successExpected flag which says that we expect the authentication finishes with success (in the first or second
* round - which depends on the continuationExpected param)
*/
private void assertSpnegoWorkflow(URI uri, final String[] mechTypesOids, final byte[] initMechToken,
final byte[] responseToken, boolean continuationExpected, boolean successExpected)
throws IOException {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
final HttpGet httpGet = new HttpGet(uri);
HttpResponse response = httpClient.execute(httpGet);
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatusLine().getStatusCode());
assertHttpHeader(response, HEADER_WWW_AUTHENTICATE, "Negotiate");
EntityUtils.consume(response.getEntity());
byte[] spnegoInitToken = KerberosTestUtils.generateSpnegoTokenInit(initMechToken, mechTypesOids);
httpGet.setHeader(HEADER_AUTHORIZATION, "Negotiate " + Base64.getEncoder().encodeToString(spnegoInitToken));
response = httpClient.execute(httpGet);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Negotiate response in HTTP header:\n" + KerberosTestUtils.dumpNegotiateHeader(response));
}
if (continuationExpected) {
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatusLine().getStatusCode());
// Assume that the server selects Kerberos v5 mechanism - OID: '1 2 840 113554 1 2 2'
assertHttpHeader(response, HEADER_WWW_AUTHENTICATE, HEADER_VAL_SELECT_KERBEROS_MECH);
EntityUtils.consume(response.getEntity());
byte[] spnegoRespToken = KerberosTestUtils.generateSpnegoTokenResp(responseToken);
httpGet.setHeader(HEADER_AUTHORIZATION, "Negotiate " + Base64.getEncoder().encodeToString(spnegoRespToken));
response = httpClient.execute(httpGet);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Negotiate response in HTTP header:\n" + KerberosTestUtils.dumpNegotiateHeader(response));
}
}
if (successExpected) {
assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
assertEquals("Unexpected response body", SimpleSecuredServlet.RESPONSE_BODY,
EntityUtils.toString(response.getEntity()));
} else {
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatusLine().getStatusCode());
assertHttpHeader(response, HEADER_WWW_AUTHENTICATE,
// if this is the first round we expect the REJECTED response.
"Negotiate" + (continuationExpected ? "" : " oQcwBaADCgEC"));
}
}
}
// Embedded classes ------------------------------------------------------
/**
* A server setup task which configures and starts Kerberos KDC server.
*/
//@formatter:off
@CreateDS(
name = "JBossDS-SPNEGOLoginModuleTestCase",
factory = org.jboss.as.test.integration.ldap.InMemoryDirectoryServiceFactory.class,
partitions =
{
@CreatePartition(
name = "jboss",
suffix = "dc=jboss,dc=org",
contextEntry = @ContextEntry(
entryLdif =
"dn: dc=jboss,dc=org\n" +
"dc: jboss\n" +
"objectClass: top\n" +
"objectClass: domain\n\n"),
indexes =
{
@CreateIndex(attribute = "objectClass"),
@CreateIndex(attribute = "dc"),
@CreateIndex(attribute = "ou")
})
},
additionalInterceptors = {KeyDerivationInterceptor.class})
@CreateKdcServer(primaryRealm = "JBOSS.ORG",
kdcPrincipal = "krbtgt/JBOSS.ORG@JBOSS.ORG",
searchBaseDn = "dc=jboss,dc=org",
transports =
{
@CreateTransport(protocol = "UDP", port = 6088)
})
//@formatter:on
static class KDCServerSetupTask implements ServerSetupTask {
private DirectoryService directoryService;
private KdcServer kdcServer;
private boolean removeBouncyCastle = false;
/**
* Creates directory services, starts LDAP server and KDCServer
*
* @param managementClient
* @param containerId
* @throws Exception
* @see org.jboss.as.arquillian.api.ServerSetupTask#setup(org.jboss.as.arquillian.container.ManagementClient,
* java.lang.String)
*/
@Override
public void setup(ManagementClient managementClient, String containerId) throws Exception {
try {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
removeBouncyCastle = true;
}
} catch (SecurityException ex) {
LOGGER.warn("Cannot register BouncyCastleProvider", ex);
}
directoryService = DSAnnotationProcessor.getDirectoryService();
final String hostname = Utils.getCannonicalHost(managementClient);
final Map<String, String> map = new HashMap<String, String>();
map.put("hostname", NetworkUtils.formatPossibleIpv6Address(hostname));
final String ldifContent = StrSubstitutor.replace(
IOUtils.toString(
SPNEGOLoginModuleTestCase.class.getResourceAsStream(SPNEGOLoginModuleTestCase.class.getSimpleName()
+ ".ldif"), "UTF-8"), map);
LOGGER.trace(ldifContent);
final SchemaManager schemaManager = directoryService.getSchemaManager();
try {
for (LdifEntry ldifEntry : new LdifReader(IOUtils.toInputStream(ldifContent))) {
directoryService.getAdminSession().add(new DefaultEntry(schemaManager, ldifEntry.getEntry()));
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
kdcServer = KDCServerAnnotationProcessor.getKdcServer(directoryService, 1024, hostname);
}
/**
* Stops LDAP server and KDCServer and shuts down the directory service.
*
* @param managementClient
* @param containerId
* @throws Exception
* @see org.jboss.as.arquillian.api.ServerSetupTask#tearDown(org.jboss.as.arquillian.container.ManagementClient,
* java.lang.String)
*/
@Override
public void tearDown(ManagementClient managementClient, String containerId) throws Exception {
kdcServer.stop();
directoryService.shutdown();
FileUtils.deleteDirectory(directoryService.getInstanceLayout().getInstanceDirectory());
if (removeBouncyCastle) {
try {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
} catch (SecurityException ex) {
LOGGER.warn("Cannot deregister BouncyCastleProvider", ex);
}
}
}
}
/**
* A {@link ServerSetupTask} instance which creates security domains for this test case.
*
* @author Josef Cacek
*/
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 SecurityModule.Builder kerberosModuleBuilder = new SecurityModule.Builder();
if (Utils.IBM_JDK) {
// http://www.ibm.com/developerworks/java/jdk/security/60/secguides/jgssDocs/api/com/ibm/security/auth/module/Krb5LoginModule.html
// http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=%2Frzaha%2Frzahajgssusejaas20.htm
// TODO Handle class name on AS side?
kerberosModuleBuilder.name("com.ibm.security.auth.module.Krb5LoginModule") //
.putOption("useKeytab", Krb5ConfServerSetupTask.HTTP_KEYTAB_FILE.toURI().toString()) //
.putOption("credsType", "acceptor");
} else {
kerberosModuleBuilder.name("Kerberos") //
.putOption("storeKey", TRUE) //
.putOption("refreshKrb5Config", TRUE) //
.putOption("useKeyTab", TRUE) //
.putOption("keyTab", Krb5ConfServerSetupTask.getKeyTabFullPath()) //
.putOption("doNotPrompt", TRUE);
}
final String host = NetworkUtils.formatPossibleIpv6Address(Utils.getCannonicalHost(managementClient));
kerberosModuleBuilder.putOption("principal", "HTTP/" + host + "@JBOSS.ORG"); //
//.putOption("debug", Boolean.FALSE.toString());
final SecurityDomain hostDomain = new SecurityDomain.Builder().name("host")
.loginModules(kerberosModuleBuilder.build()) //
.build();
final SecurityDomain spnegoDomain = new SecurityDomain.Builder()
.name("SPNEGO")
.loginModules(
new SecurityModule.Builder().name("SPNEGO").putOption("password-stacking", "useFirstPass")
.putOption("serverSecurityDomain", "host").build()) //
.mappingModules(
new SecurityModule.Builder().name("SimpleRoles")
.putOption("jduke@JBOSS.ORG", "Admin,Users,JBossAdmin,TestRole").build())//
.build();
final SecurityDomain spnegoWithFallback = new SecurityDomain.Builder()
.name("SPNEGO-with-fallback")
.loginModules(new SecurityModule.Builder().name("SPNEGO") //
.putOption("password-stacking", "useFirstPass") //
.putOption("serverSecurityDomain", "host") //
.putOption("usernamePasswordDomain", "FORM-as-fallback") //
.build())
.mappingModules(
new SecurityModule.Builder().name("SimpleRoles")
.putOption("jduke@JBOSS.ORG", "Admin,Users,JBossAdmin,TestRole").build())//
.build();
final SecurityDomain formFallbackDomain = new SecurityDomain.Builder().name("FORM-as-fallback")
.loginModules(new SecurityModule.Builder().name("UsersRoles") //
.putOption("usersProperties", "fallback-users.properties") //
.putOption("rolesProperties", "fallback-roles.properties") //
.build()).build();
return new SecurityDomain[]{hostDomain, spnegoDomain, spnegoWithFallback, formFallbackDomain};
}
}
/**
* A Kerberos system-properties server setup task. Sets path to a <code>krb5.conf</code> file and enables Kerberos debug
* messages.
*
* @author Josef Cacek
*/
static class KerberosSystemPropertiesSetupTask extends AbstractSystemPropertiesServerSetupTask {
/**
* Returns "java.security.krb5.conf" and "sun.security.krb5.debug" properties.
*
* @return Kerberos properties
* @see org.jboss.as.test.integration.security.common.AbstractSystemPropertiesServerSetupTask#getSystemProperties()
*/
@Override
protected SystemProperty[] getSystemProperties() {
final Map<String, String> map = new HashMap<String, String>();
map.put("java.security.krb5.conf", Krb5ConfServerSetupTask.getKrb5ConfFullPath());
//map.put("sun.security.krb5.debug", TRUE);
map.put(SecurityConstants.DISABLE_SECDOMAIN_OPTION, TRUE);
return mapToSystemProperties(map);
}
}
}