/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.testsuite.federation.kerberos; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; import java.net.URI; import java.security.Principal; import java.util.Hashtable; import java.util.List; import java.util.Map; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.security.sasl.Sasl; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import org.apache.http.NameValuePair; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.client.params.AuthPolicy; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.ietf.jgss.GSSCredential; import org.jboss.arquillian.graphene.page.Page; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine; import org.junit.After; import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.common.util.KerberosSerializationUtils; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.events.Details; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.testsuite.AbstractAuthTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.pages.AccountPasswordPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.OAuthClient; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public abstract class AbstractKerberosTest extends AbstractAuthTest { protected KeycloakSPNegoSchemeFactory spnegoSchemeFactory; protected ResteasyClient client; @Page protected LoginPage loginPage; @Rule public AssertEvents events = new AssertEvents(this); @Page protected AccountPasswordPage changePasswordPage; protected abstract CommonKerberosConfig getKerberosConfig(); protected abstract ComponentRepresentation getUserStorageConfiguration(); protected abstract void setKrb5ConfPath(); protected abstract boolean isStartEmbeddedLdapServer(); @Override public void addTestRealms(List<RealmRepresentation> testRealms) { RealmRepresentation realmRep = loadJson(getClass().getResourceAsStream("/kerberos/kerberosrealm.json"), RealmRepresentation.class); testRealms.add(realmRep); } @Before public void beforeAbstractKeycloakTest() throws Exception { super.beforeAbstractKeycloakTest(); testRealmPage.setAuthRealm(AuthRealm.TEST); changePasswordPage.realm(AuthRealm.TEST); setKrb5ConfPath(); spnegoSchemeFactory = new KeycloakSPNegoSchemeFactory(getKerberosConfig()); initHttpClient(true); removeAllUsers(); oauth.clientId("kerberos-app"); ComponentRepresentation rep = getUserStorageConfiguration(); Response resp = testRealmResource().components().add(rep); getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); resp.close(); } @After public void afterAbstractKeycloakTest() { cleanupApacheHttpClient(); super.afterAbstractKeycloakTest(); } private void cleanupApacheHttpClient() { client.close(); client = null; } // @Test // public void sleepTest() throws Exception { // String kcLoginPageLocation = oauth.getLoginFormUrl(); // Thread.sleep(10000000); // } @Test public void spnegoNotAvailableTest() throws Exception { initHttpClient(false); String kcLoginPageLocation = oauth.getLoginFormUrl(); Response response = client.target(kcLoginPageLocation).request().get(); Assert.assertEquals(401, response.getStatus()); Assert.assertEquals(KerberosConstants.NEGOTIATE, response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE)); String responseText = response.readEntity(String.class); response.close(); } protected OAuthClient.AccessTokenResponse spnegoLoginTestImpl() throws Exception { Response spnegoResponse = spnegoLogin("hnelson", "secret"); Assert.assertEquals(302, spnegoResponse.getStatus()); List<UserRepresentation> users = testRealmResource().users().search("hnelson", 0, 1); String userId = users.get(0).getId(); events.expectLogin() .client("kerberos-app") .user(userId) .detail(Details.USERNAME, "hnelson") .assertEvent(); String codeUrl = spnegoResponse.getLocation().toString(); return assertAuthenticationSuccess(codeUrl); } protected abstract boolean isCaseSensitiveLogin(); // KEYCLOAK-2102 @Test public void spnegoCaseInsensitiveTest() throws Exception { Response spnegoResponse = spnegoLogin(isCaseSensitiveLogin() ? "MyDuke" : "myduke", "theduke"); Assert.assertEquals(302, spnegoResponse.getStatus()); List<UserRepresentation> users = testRealmResource().users().search("myduke", 0, 1); String userId = users.get(0).getId(); events.expectLogin() .client("kerberos-app") .user(userId) .detail(Details.USERNAME, "myduke") .assertEvent(); String codeUrl = spnegoResponse.getLocation().toString(); assertAuthenticationSuccess(codeUrl); } @Test public void usernamePasswordLoginTest() throws Exception { // Change editMode to READ_ONLY updateProviderEditMode(UserStorageProvider.EditMode.READ_ONLY); // Login with username/password from kerberos changePasswordPage.open(); loginPage.assertCurrent(); loginPage.login("jduke", "theduke"); changePasswordPage.assertCurrent(); // Bad existing password changePasswordPage.changePassword("theduke-invalid", "newPass", "newPass"); Assert.assertTrue(driver.getPageSource().contains("Invalid existing password.")); // Change password is not possible as editMode is READ_ONLY changePasswordPage.changePassword("theduke", "newPass", "newPass"); Assert.assertTrue( driver.getPageSource().contains("You can't update your password as your account is read only")); // Change editMode to UNSYNCED updateProviderEditMode(UserStorageProvider.EditMode.UNSYNCED); // Successfully change password now changePasswordPage.changePassword("theduke", "newPass", "newPass"); Assert.assertTrue(driver.getPageSource().contains("Your password has been updated.")); changePasswordPage.logout(); // Login with old password doesn't work, but with new password works loginPage.login("jduke", "theduke"); loginPage.assertCurrent(); loginPage.login("jduke", "newPass"); changePasswordPage.assertCurrent(); changePasswordPage.logout(); // Assert SPNEGO login still with the old password as mode is unsynced events.clear(); Response spnegoResponse = spnegoLogin("jduke", "theduke"); Assert.assertEquals(302, spnegoResponse.getStatus()); List<UserRepresentation> users = testRealmResource().users().search("jduke", 0, 1); String userId = users.get(0).getId(); events.expectLogin() .client("kerberos-app") .user(userId) .detail(Details.USERNAME, "jduke") .assertEvent(); String codeUrl = spnegoResponse.getLocation().toString(); assertAuthenticationSuccess(codeUrl); } @Test public void credentialDelegationTest() throws Exception { Assume.assumeTrue("Ignoring test as the embedded server is not started", isStartEmbeddedLdapServer()); // Add kerberos delegation credential mapper ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, KerberosConstants.GSS_DELEGATION_CREDENTIAL, KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String", true, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, true, false); ProtocolMapperRepresentation protocolMapperRep = ModelToRepresentation.toRepresentation(protocolMapper); ClientResource clientResource = findClientByClientId(testRealmResource(), "kerberos-app"); Response response = clientResource.getProtocolMappers().createMapper(protocolMapperRep); String protocolMapperId = ApiUtil.getCreatedId(response); response.close(); // SPNEGO login OAuthClient.AccessTokenResponse tokenResponse = spnegoLoginTestImpl(); // Assert kerberos ticket in the accessToken can be re-used to authenticate against other 3rd party kerberos service (ApacheDS Server in this case) String accessToken = tokenResponse.getAccessToken(); AccessToken token = oauth.verifyToken(accessToken); String serializedGssCredential = (String) token.getOtherClaims().get(KerberosConstants.GSS_DELEGATION_CREDENTIAL); Assert.assertNotNull(serializedGssCredential); GSSCredential gssCredential = KerberosSerializationUtils.deserializeCredential(serializedGssCredential); String ldapResponse = invokeLdap(gssCredential, token.getPreferredUsername()); Assert.assertEquals("Horatio Nelson", ldapResponse); // Logout oauth.openLogout(); // Remove protocolMapper clientResource.getProtocolMappers().delete(protocolMapperId); // Login and assert delegated credential not anymore tokenResponse = spnegoLoginTestImpl(); accessToken = tokenResponse.getAccessToken(); token = oauth.verifyToken(accessToken); Assert.assertFalse(token.getOtherClaims().containsKey(KerberosConstants.GSS_DELEGATION_CREDENTIAL)); events.clear(); } private String invokeLdap(GSSCredential gssCredential, String username) throws NamingException { Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:10389"); if (gssCredential != null) { env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); env.put(Sasl.CREDENTIALS, gssCredential); } DirContext ctx = new InitialDirContext(env); try { Attributes attrs = ctx.getAttributes("uid=" + username + ",ou=People,dc=keycloak,dc=org"); String cn = (String) attrs.get("cn").get(); String sn = (String) attrs.get("sn").get(); return cn + " " + sn; } finally { ctx.close(); } } protected Response spnegoLogin(String username, String password) { String kcLoginPageLocation = oauth.getLoginFormUrl(); // Request for SPNEGO login sent with Resteasy client spnegoSchemeFactory.setCredentials(username, password); Response response = client.target(kcLoginPageLocation).request().get(); if (response.getStatus() == 302) { if (response.getLocation() == null) return response; String uri = response.getLocation().toString(); if (uri.contains("login-actions/required-action")) { response = client.target(uri).request().get(); } } return response; } protected void initHttpClient(boolean useSpnego) { if (client != null) { cleanupApacheHttpClient(); } DefaultHttpClient httpClient = (DefaultHttpClient) new HttpClientBuilder() .disableCookieCache(false) .build(); httpClient.getAuthSchemes().register(AuthPolicy.SPNEGO, spnegoSchemeFactory); if (useSpnego) { Credentials fake = new Credentials() { public String getPassword() { return null; } public Principal getUserPrincipal() { return null; } }; httpClient.getCredentialsProvider().setCredentials( new AuthScope(null, -1, null), fake); } ApacheHttpClient4Engine engine = new ApacheHttpClient4Engine(httpClient); client = new ResteasyClientBuilder().httpEngine(engine).build(); } protected void removeAllUsers() { RealmResource realm = testRealmResource(); List<UserRepresentation> users = realm.users().search("", 0, Integer.MAX_VALUE); for (UserRepresentation user : users) { if (!user.getUsername().equals(AssertEvents.DEFAULT_USERNAME)) { realm.users().get(user.getId()).remove(); } } Assert.assertEquals(1, realm.users().search("", 0, Integer.MAX_VALUE).size()); } protected void assertUser(String expectedUsername, String expectedEmail, String expectedFirstname, String expectedLastname, boolean updateProfileActionExpected) { try { UserRepresentation user = ApiUtil.findUserByUsername(testRealmResource(), expectedUsername); Assert.assertNotNull(user); Assert.assertEquals(expectedEmail, user.getEmail()); Assert.assertEquals(expectedFirstname, user.getFirstName()); Assert.assertEquals(expectedLastname, user.getLastName()); if (updateProfileActionExpected) { Assert.assertEquals(UserModel.RequiredAction.UPDATE_PROFILE.toString(), user.getRequiredActions().iterator().next()); } else { Assert.assertTrue(user.getRequiredActions().isEmpty()); } } finally { } } protected OAuthClient.AccessTokenResponse assertAuthenticationSuccess(String codeUrl) throws Exception { List<NameValuePair> pairs = URLEncodedUtils.parse(new URI(codeUrl), "UTF-8"); String code = null; String state = null; for (NameValuePair pair : pairs) { if (pair.getName().equals(OAuth2Constants.CODE)) { code = pair.getValue(); } else if (pair.getName().equals(OAuth2Constants.STATE)) { state = pair.getValue(); } } Assert.assertNotNull(code); Assert.assertNotNull(state); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); Assert.assertNotNull(response.getAccessToken()); events.clear(); return response; } protected void updateProviderEditMode(UserStorageProvider.EditMode editMode) { List<ComponentRepresentation> reps = testRealmResource().components().query("test", UserStorageProvider.class.getName()); Assert.assertEquals(1, reps.size()); ComponentRepresentation kerberosProvider = reps.get(0); kerberosProvider.getConfig().putSingle(LDAPConstants.EDIT_MODE, editMode.toString()); testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider); } public RealmResource testRealmResource() { return adminClient.realm("test"); } // TODO: Use LDAPTestUtils.toComponentConfig once it's migrated to new testsuite public static MultivaluedHashMap<String, String> toComponentConfig(Map<String, String> ldapConfig) { MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>(); for (Map.Entry<String, String> entry : ldapConfig.entrySet()) { config.add(entry.getKey(), entry.getValue()); } return config; } }