/* * 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.picketlink; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat; 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.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.naming.Context; 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.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.params.ClientPNames; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.util.EntityUtils; import org.hamcrest.Matcher; 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.security.Constants; import org.jboss.as.test.integration.security.common.AbstractKrb5ConfServerSetupTask; 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.NullHCCredentials; 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.JBossNegotiateSchemeFactory; import org.jboss.as.test.integration.security.common.negotiation.KerberosTestUtils; import org.jboss.as.test.integration.security.common.servlets.PrincipalPrintingServlet; import org.jboss.as.test.integration.security.common.servlets.RolePrintingServlet; import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.ShrinkWrap; 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; /** * Tests for integration of the IDP and Kerberos. * * @author Hynek Mlnarik */ @RunWith(Arquillian.class) @ServerSetup({ KerberosServerSetupTask.Krb5ConfServerSetupTask.class, KerberosServerSetupTask.SystemPropertiesSetup.class, KerberosServerSetupTask.class, SAML2KerberosAuthenticationTestCase.SecurityDomainsSetup.class }) @RunAsClient @Ignore("AS7-6796 - Undertow SPNEGO") public class SAML2KerberosAuthenticationTestCase { private static final String SERVICE_PROVIDER_NAME = "SP_DEPLOYMENT"; private static final String IDENTITY_PROVIDER_NAME = "IDP_DEPLOYMENT"; private static final String SP_DEPLOYMENT_NAME = "test-" + SERVICE_PROVIDER_NAME; private static final String IDP_DEPLOYMENT_NAME = "idp-test-" + SERVICE_PROVIDER_NAME; private static final String SERVICE_PROVIDER_REALM = "spRealm"; private static final String IDENTITY_PROVIDER_REALM = IDP_DEPLOYMENT_NAME; private static final Logger LOGGER = Logger.getLogger(SAML2KerberosAuthenticationTestCase.class); private static final String PICKETLINK_MODULE_NAME = "org.picketlink"; private static final String JBOSS_NEGOTIATION_MODULE_NAME = "org.jboss.security.negotiation"; private static final String DUKE_PASSWORD = "theduke"; @ArquillianResource ManagementClient mgmtClient; private static void consumeResponse(final HttpResponse response) { HttpEntity entity = response.getEntity(); EntityUtils.consumeQuietly(entity); } // Public methods -------------------------------------------------------- /** * Skip unsupported/unstable/buggy Kerberos configurations. */ @Before public static void before() { KerberosTestUtils.assumeKerberosAuthenticationSupported(); } /** * Creates a {@link WebArchive} for given security domain. * * @return */ @Deployment(name = SERVICE_PROVIDER_NAME) public static WebArchive createSpWar() { final WebArchive war = ShrinkWrap.create(WebArchive.class, SP_DEPLOYMENT_NAME + ".war"); war.addClasses(RolePrintingServlet.class, PrincipalPrintingServlet.class); war.addAsWebInfResource(SAML2KerberosAuthenticationTestCase.class.getPackage(), SAML2KerberosAuthenticationTestCase.class.getSimpleName() + "-web.xml", "web.xml"); war.addAsWebInfResource(Utils.getJBossWebXmlAsset(SERVICE_PROVIDER_REALM, "org.picketlink.identity.federation.bindings.tomcat.sp.ServiceProviderAuthenticator"), "jboss-web.xml"); war.addAsManifestResource(Utils.getJBossDeploymentStructure(PICKETLINK_MODULE_NAME, JBOSS_NEGOTIATION_MODULE_NAME), "jboss-deployment-structure.xml"); war.addAsWebInfResource( new StringAsset(PicketLinkTestBase.propertiesReplacer("picketlink-sp.xml", SP_DEPLOYMENT_NAME, "REDIRECT", IDP_DEPLOYMENT_NAME)), "picketlink.xml"); war.add(new StringAsset("Welcome to deployment: " + SP_DEPLOYMENT_NAME), "index.jsp"); return war; } /** * Creates a {@link WebArchive} for given security domain. * * @return */ @Deployment(name = IDENTITY_PROVIDER_NAME) public static WebArchive createIdpWar() { final WebArchive war = ShrinkWrap.create(WebArchive.class, IDP_DEPLOYMENT_NAME + ".war"); war.addAsWebInfResource(SAML2KerberosAuthenticationTestCase.class.getPackage(), SAML2KerberosAuthenticationTestCase.class.getSimpleName() + "-idp-web.xml", "web.xml"); war.addAsWebInfResource(Utils.getJBossWebXmlAsset(IDP_DEPLOYMENT_NAME, "org.jboss.security.negotiation.NegotiationAuthenticator", "org.picketlink.identity.federation.bindings.tomcat.idp.IDPWebBrowserSSOValve"), "jboss-web.xml"); war.addAsManifestResource(Utils.getJBossDeploymentStructure(PICKETLINK_MODULE_NAME, JBOSS_NEGOTIATION_MODULE_NAME), "jboss-deployment-structure.xml"); war.addAsWebInfResource( new StringAsset(PicketLinkTestBase.propertiesReplacer("picketlink-idp.xml", IDP_DEPLOYMENT_NAME, "", IDP_DEPLOYMENT_NAME)), "picketlink.xml"); war.addAsWebResource(SAML2KerberosAuthenticationTestCase.class.getPackage(), "error.jsp", "error.jsp"); war.addAsWebResource(SAML2KerberosAuthenticationTestCase.class.getPackage(), "login.jsp", "login.jsp"); war.add(new StringAsset("Welcome to IdP"), "index.jsp"); war.add(new StringAsset("Welcome to IdP hosted"), "hosted/index.jsp"); return war; } /** * Test for SPNEGO working. * * @throws Exception */ @Test @OperateOnDeployment(SERVICE_PROVIDER_NAME) public void testNegotiateHttpHeader(@ArquillianResource URL webAppURL, @ArquillianResource @OperateOnDeployment(IDENTITY_PROVIDER_NAME) URL idpURL) throws Exception { try (CloseableHttpClient httpClient = HttpClients.createDefault()){ final HttpGet httpGet = new HttpGet(webAppURL.toURI()); final HttpResponse response = httpClient.execute(httpGet); assertThat("Unexpected status code.", response.getStatusLine().getStatusCode(), equalTo(HttpServletResponse.SC_UNAUTHORIZED)); final Header[] authnHeaders = response.getHeaders("WWW-Authenticate"); assertThat("WWW-Authenticate header is present", authnHeaders, notNullValue()); assertThat("WWW-Authenticate header is non-empty", authnHeaders.length, not(equalTo(0))); final Set<? super String> authnHeaderValues = new HashSet<String>(); for (final Header header : authnHeaders) { authnHeaderValues.add(header.getValue()); } Matcher<String> matcherContainsString = containsString("Negotiate"); Matcher<Iterable<? super String>> matcherAnyContainsNegotiate = hasItem(matcherContainsString); assertThat("WWW-Authenticate [Negotiate] header is missing", authnHeaderValues, matcherAnyContainsNegotiate); consumeResponse(response); } } /** * Test roles for jduke user. * * @throws Exception */ @Test @OperateOnDeployment(SERVICE_PROVIDER_NAME) public void testJDukeRoles(@ArquillianResource URL webAppURL, @ArquillianResource @OperateOnDeployment(IDENTITY_PROVIDER_NAME) URL idpURL) throws Exception { final URI rolesPrintingURL = new URI(webAppURL.toExternalForm() + RolePrintingServlet.SERVLET_PATH.substring(1) + "?test=testDeploymentViaKerberos&" + KerberosServerSetupTask.QUERY_ROLES); String responseBody = makeCallWithKerberosAuthn(rolesPrintingURL, idpURL.toURI(), "jduke", DUKE_PASSWORD); final List<String> assignedRolesList = Arrays.asList("TheDuke", "Echo", "Admin"); for (String role : KerberosServerSetupTask.ROLE_NAMES) { if (assignedRolesList.contains(role)) { assertThat("Missing role assignment", responseBody, containsString("," + role + ",")); } else { assertThat("Unexpected role assignment", responseBody, not(containsString("," + role + ","))); } } } /** * Test principal for jduke user. * * @throws Exception */ @Test @OperateOnDeployment(SERVICE_PROVIDER_NAME) public void testJDukePrincipal(@ArquillianResource URL webAppURL, @ArquillianResource @OperateOnDeployment(IDENTITY_PROVIDER_NAME) URL idpURL) throws Exception { final String cannonicalHost = Utils.getCannonicalHost(mgmtClient); final URI principalPrintingURL = new URI(webAppURL.toExternalForm() + PrincipalPrintingServlet.SERVLET_PATH.substring(1) + "?test=testDeploymentViaKerberos"); String responseBody = makeCallWithKerberosAuthn(principalPrintingURL, Utils.replaceHost(idpURL.toURI(), cannonicalHost), "jduke", DUKE_PASSWORD); assertThat("Unexpected principal", responseBody, equalTo("jduke")); } // Private methods ------------------------------------------------------- /** * 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 * @return HTTP response body * @throws IOException * @throws URISyntaxException * @throws PrivilegedActionException * @throws LoginException */ public static String makeCallWithKerberosAuthn(URI uri, URI idpUri, final String user, final String pass) throws IOException, URISyntaxException, PrivilegedActionException, LoginException { final String canonicalHost = Utils.getDefaultHost(true); uri = Utils.replaceHost(uri, canonicalHost); idpUri = Utils.replaceHost(idpUri, canonicalHost); LOGGER.trace("Making call to: " + uri); LOGGER.trace("Expected IDP: " + idpUri); final Krb5LoginConfiguration krb5configuration = new Krb5LoginConfiguration(Utils.getLoginConfiguration()); // Use our custom configuration to avoid reliance on external config 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 HttpGetInKerberos(uri, idpUri)); lc.logout(); krb5configuration.resetConfiguration(); return responseBody; } // Inner classes ------------------------------------------------------ /** * A {@link ServerSetupTask} instance which creates security domains for this test case. * * @author Hynek Mlnarik */ static class SecurityDomainsSetup extends AbstractSecurityDomainsServerSetupTask { private static final String SERVER_SECURITY_DOMAIN = "host"; /** * Returns SecurityDomains configuration for this testcase. * * @see org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask#getSecurityDomains() */ @Override protected SecurityDomain[] getSecurityDomains() { List<SecurityDomain> res = new LinkedList<SecurityDomain>(); // Add host security domain res.add(new SecurityDomain.Builder() .name(SERVER_SECURITY_DOMAIN) .cacheType("default") .loginModules( new SecurityModule.Builder() .name(Krb5LoginConfiguration.getLoginModule()) .flag(Constants.REQUIRED) .options( Krb5LoginConfiguration.getOptions( KerberosServerSetupTask.getHttpServicePrincipal(managementClient), AbstractKrb5ConfServerSetupTask.HTTP_KEYTAB_FILE, true)).build()).build()); // Add IdP security domain res.add(new SecurityDomain.Builder() .name(IDENTITY_PROVIDER_REALM) .loginModules( new SecurityModule.Builder() // Login module used for password negotiation .name("SPNEGO").flag(Constants.REQUISITE).putOption("password-stacking", "useFirstPass") .putOption("serverSecurityDomain", SERVER_SECURITY_DOMAIN) .putOption("removeRealmFromPrincipal", "true").build(), new SecurityModule.Builder() // Login module used for role retrieval .name("org.jboss.security.auth.spi.LdapExtLoginModule") .flag(Constants.REQUIRED) .putOption("password-stacking", "useFirstPass") .putOption( Context.PROVIDER_URL, "ldap://" + NetworkUtils.formatPossibleIpv6Address(Utils .getCannonicalHost(managementClient)) + ":" + KerberosServerSetupTask.LDAP_PORT) .putOption("baseCtxDN", "ou=People,dc=jboss,dc=org").putOption("baseFilter", "(uid={0})") .putOption("rolesCtxDN", "ou=Roles,dc=jboss,dc=org") .putOption("roleFilter", "(|(objectClass=referral)(member={1}))") .putOption("roleAttributeID", "cn").putOption("referralUserAttributeIDToCheck", "member") .putOption("bindDN", KerberosServerSetupTask.SECURITY_PRINCIPAL) .putOption("bindCredential", KerberosServerSetupTask.SECURITY_CREDENTIALS) .putOption(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory") .putOption(Context.SECURITY_AUTHENTICATION, "simple").putOption(Context.REFERRAL, "follow") .putOption("throwValidateError", "true").putOption("roleRecursion", "5").build()).build()); // Add SP security domain res.add(new SecurityDomain.Builder() .name(SERVICE_PROVIDER_REALM) .loginModules( new SecurityModule.Builder() .name("org.picketlink.identity.federation.bindings.jboss.auth.SAML2LoginModule") .flag(Constants.REQUIRED).build()).build()); return res.toArray(new SecurityDomain[0]); } } /** * Class which is intended to be run in context of a Kerberos-authenticated user, to test the http authentication via IdP. */ private static class HttpGetInKerberos implements PrivilegedExceptionAction<String> { private final URI uri; private final URI idpUri; /** * Initializes the instance. * * @param uri URI of the web application * @param idpUri URI of the respective identity provider */ HttpGetInKerberos(URI uri, URI idpUri) { this.uri = uri; this.idpUri = idpUri; } /** * Performs authentication via IdP and retrieves the document body from the {@link #uri}. * * @return Body of the response retrieved from {@link #uri} * @throws Exception */ @Override public String run() throws Exception { 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 DefaultHttpClient httpClient = new DefaultHttpClient(); httpClient.getAuthSchemes().register(AuthPolicy.SPNEGO, new JBossNegotiateSchemeFactory(true)); httpClient.getCredentialsProvider().setCredentials(new AuthScope(null, -1, null), new NullHCCredentials()); */ final HttpParams doNotRedirect = new BasicHttpParams(); doNotRedirect.setParameter(ClientPNames.HANDLE_REDIRECTS, false); doNotRedirect.setParameter(ClientPNames.HANDLE_AUTHENTICATION, true); final HttpParams doRedirect = new BasicHttpParams(); doRedirect.setParameter(ClientPNames.HANDLE_AUTHENTICATION, true); doRedirect.setParameter(ClientPNames.HANDLE_REDIRECTS, true); try (final CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultAuthSchemeRegistry(authSchemeRegistry) .setDefaultCredentialsProvider(credentialsProvider) .build()){ // 1. Login to IdP HttpGet initialIdpHttpGet = new HttpGet(this.idpUri); // GET /idp-test-DEP1 initialIdpHttpGet.setParams(doRedirect); HttpResponse response = httpClient.execute(initialIdpHttpGet); assertThat("Unexpected status code when expecting successfull kerberos authentication", response .getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_OK)); consumeResponse(response); // 2. Do the work, manually do the redirect HttpGet initialHttpGet = new HttpGet(this.uri); // GET /test-DEP1/printRoles?role=TheDuke2&role=... initialHttpGet.setParams(doNotRedirect); response = httpClient.execute(initialHttpGet); assertThat("Unexpected status code when expecting redirect to IdP", response.getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_MOVED_TEMPORARILY)); String initialHttpGetRedirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); consumeResponse(response); HttpGet idpHttpGet = new HttpGet(initialHttpGetRedirect); // GET /idp-test-DEP1/?SAMLRequest=jZLfT4MwEMf..... idpHttpGet.setParams(doNotRedirect); response = httpClient.execute(idpHttpGet); assertThat("Unexpected status code when expecting redirect from SP with SAML request", response.getStatusLine() .getStatusCode(), equalTo(HttpStatus.SC_MOVED_TEMPORARILY)); String idpHttpGetRedirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); consumeResponse(response); HttpGet idpHttpGetRedirectForAuth = new HttpGet(idpHttpGetRedirect); // GET // /idp-test-DEP1/?SAMLRequest=jZLfT4MwEMf....., // Authorization: Negotiate idpHttpGetRedirectForAuth.setParams(doNotRedirect); response = httpClient.execute(idpHttpGetRedirectForAuth); assertThat("Unexpected status code when expecting redirect from IdP with SAML response", response .getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_MOVED_TEMPORARILY)); String idpHttpGetRedirectAuth = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); consumeResponse(response); HttpGet spHttpGet = new HttpGet(idpHttpGetRedirectAuth); // GET /test-DEP1/?SAMLResponse=... spHttpGet.setParams(doNotRedirect); response = httpClient.execute(spHttpGet); assertThat("Unexpected status code when expecting succesfull authentication to the SP", response .getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_OK)); return EntityUtils.toString(response.getEntity()); } } } }