KEYCLOAK-6038 Kerberos cross-realm trust test

This commit is contained in:
mposolda 2018-08-08 09:02:03 +02:00 committed by Pavel Drozd
parent 060b3b8d0f
commit 575851d45c
25 changed files with 844 additions and 268 deletions

View file

@ -40,7 +40,7 @@ public class KerberosUsernamePasswordAuthenticator {
private static final Logger logger = Logger.getLogger(KerberosUsernamePasswordAuthenticator.class); private static final Logger logger = Logger.getLogger(KerberosUsernamePasswordAuthenticator.class);
private final CommonKerberosConfig config; protected final CommonKerberosConfig config;
private LoginContext loginContext; private LoginContext loginContext;
public KerberosUsernamePasswordAuthenticator(CommonKerberosConfig config) { public KerberosUsernamePasswordAuthenticator(CommonKerberosConfig config) {

View file

@ -111,7 +111,14 @@ To start a ApacheDS based Kerberos server for testing Kerberos + LDAP sending ru
mvn exec:java -Pkerberos mvn exec:java -Pkerberos
There are additional system properties you can use to configure (See LDAPEmbeddedServer and KerberosEmbeddedServer class for details) but for testing purposes default values should be good. There are additional system properties you can use to configure (See LDAPEmbeddedServer and KerberosEmbeddedServer class for details) but for testing purposes default values should be good.
By default ApacheDS LDAP server will be running on localhost:10389 and Kerberos KDC on localhost:6088 . By default ApacheDS LDAP server will be running on localhost:10389 and Kerberos KDC on localhost:6088 .
The alternative is to start Kerberos with the alternative realm KC2.COM instead of default KEYCLOAK.ORG.
The ApacheDS server will be then started with all ports shifted by 1000 (EG. LDAP on 11389, Kerberos KDC on 7088).
This allows to start 2 kerberos servers in parallel to test things like Kerberos cross-realm trust:
mvn exec:java -Pkerberos -Dkeycloak.kerberos.realm=KC2.COM
Once kerberos is running, you can create LDAP Federation provider in Keycloak admin console with same settings like mentioned in previous LDAP section. Once kerberos is running, you can create LDAP Federation provider in Keycloak admin console with same settings like mentioned in previous LDAP section.
But additionally you can enable Kerberos authentication in LDAP provider with the settings like: But additionally you can enable Kerberos authentication in LDAP provider with the settings like:

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest.resource;
import java.util.Map; import java.util.Map;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
@ -161,4 +162,20 @@ public class TestLDAPResource {
LDAPObject james = LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "jameskeycloak", "James", "Brown", "james@email.org", null, "8910"); LDAPObject james = LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "jameskeycloak", "James", "Brown", "james@email.org", null, "8910");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1"); LDAPTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1");
} }
/**
* Remove specified user directly just from the LDAP server
*/
@DELETE
@Path("/remove-ldap-user")
@Consumes(MediaType.APPLICATION_JSON)
public void removeLDAPUser(@QueryParam("username") String ldapUsername) {
ComponentModel ldapCompModel = LDAPTestUtils.getLdapProviderModel(session, realm);
UserStorageProviderModel ldapModel = new UserStorageProviderModel(ldapCompModel);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.removeLDAPUserByUsername(ldapProvider, realm,
ldapProvider.getLdapIdentityStore().getConfig(), ldapUsername);
}
} }

View file

@ -29,6 +29,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPConfig; import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.LDAPUtils; import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery; import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
@ -120,7 +121,7 @@ public class LDAPTestUtils {
public static ComponentModel getLdapProviderModel(KeycloakSession session, RealmModel realm) { public static ComponentModel getLdapProviderModel(KeycloakSession session, RealmModel realm) {
List<ComponentModel> components = realm.getComponents(realm.getId(), UserStorageProvider.class.getName()); List<ComponentModel> components = realm.getComponents(realm.getId(), UserStorageProvider.class.getName());
for (ComponentModel component : components) { for (ComponentModel component : components) {
if ("test-ldap".equals(component.getName())) { if (LDAPStorageProviderFactory.PROVIDER_NAME.equals(component.getProviderId())) {
return component; return component;
} }
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.client.resources;
import java.util.Map; import java.util.Map;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
@ -53,4 +54,13 @@ public interface TestingLDAPResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
void prepareGroupsLDAPTest(); void prepareGroupsLDAPTest();
/**
* Remove specified user directly just from the LDAP server
*/
@DELETE
@Path("/remove-ldap-user")
@Consumes(MediaType.APPLICATION_JSON)
void removeLDAPUser(@QueryParam("username") String ldapUsername);
} }

View file

@ -34,9 +34,11 @@ public class KerberosRule extends LDAPRule {
private static final Logger log = Logger.getLogger(KerberosRule.class); private static final Logger log = Logger.getLogger(KerberosRule.class);
private final String configLocation; private final String configLocation;
private final String kerberosRealm;
public KerberosRule(String configLocation) { public KerberosRule(String configLocation, String kerberosRealm) {
this.configLocation = configLocation; this.configLocation = configLocation;
this.kerberosRealm = kerberosRealm;
// Global kerberos configuration // Global kerberos configuration
String krb5ConfPath = getKrb5ConfPath(); String krb5ConfPath = getKrb5ConfPath();
@ -61,11 +63,25 @@ public class KerberosRule extends LDAPRule {
return configLocation; return configLocation;
} }
public String getKerberosRealm() {
return kerberosRealm;
}
@Override @Override
protected LDAPEmbeddedServer createServer() { protected LDAPEmbeddedServer createServer() {
Properties defaultProperties = new Properties(); Properties defaultProperties = new Properties();
defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY); defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY);
defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:kerberos/users-kerberos.ldif");
KerberosEmbeddedServer.configureDefaultPropertiesForRealm(this.kerberosRealm, defaultProperties);
// Improve if more flexibility is needed
if ("KEYCLOAK.ORG".equals(kerberosRealm)) {
defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:kerberos/users-kerberos.ldif");
} else if ("KC2.COM".equals(kerberosRealm)) {
defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:kerberos/users-kerberos-kc2.ldif");
}
return new KerberosEmbeddedServer(defaultProperties); return new KerberosEmbeddedServer(defaultProperties);
} }

View file

@ -0,0 +1,173 @@
/*
* Copyright 2017 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 java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.ietf.jgss.GSSCredential;
import org.junit.Assume;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.KerberosSerializationUtils;
import org.keycloak.events.Details;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
/**
* Contains just test methods
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractKerberosSingleRealmTest extends AbstractKerberosTest {
@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();
}
// KEYCLOAK-7823
@Test
public void spnegoLoginWithRequiredKerberosAuthExecutionTest() {
AuthenticationExecutionModel.Requirement oldRequirement = updateKerberosAuthExecutionRequirement(
AuthenticationExecutionModel.Requirement.REQUIRED);
Response response = spnegoLogin("hnelson", "secret");
updateKerberosAuthExecutionRequirement(oldRequirement);
Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
}
// KEYCLOAK-2102
@Test
public void spnegoCaseInsensitiveTest() throws Exception {
assertSuccessfulSpnegoLogin(getKerberosRule().isCaseSensitiveLogin() ? "MyDuke" : "myduke", "myduke", "theduke");
}
@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", getKerberosRule().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, 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
AccessToken token = assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret");
// Assert kerberos ticket in the accessToken can be re-used to authenticate against other 3rd party kerberos service (ApacheDS Server in this case)
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
token = assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret");
Assert.assertFalse(token.getOtherClaims().containsKey(KerberosConstants.GSS_DELEGATION_CREDENTIAL));
events.clear();
}
}

View file

@ -18,7 +18,6 @@
package org.keycloak.testsuite.federation.kerberos; package org.keycloak.testsuite.federation.kerberos;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import java.net.URI; import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -34,7 +33,6 @@ import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext; import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.InitialDirContext;
import javax.security.sasl.Sasl; import javax.security.sasl.Sasl;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
@ -49,34 +47,27 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine; import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
import org.junit.After; import org.junit.After;
import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.KerberosSerializationUtils;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.testsuite.AbstractAuthTest; import org.keycloak.testsuite.AbstractAuthTest;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
@ -84,9 +75,12 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AccountPasswordPage; import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.KerberosRule;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
/** /**
* Contains just helper methods. No test methods.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public abstract class AbstractKerberosTest extends AbstractAuthTest { public abstract class AbstractKerberosTest extends AbstractAuthTest {
@ -104,13 +98,30 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
@Page @Page
protected AccountPasswordPage changePasswordPage; protected AccountPasswordPage changePasswordPage;
protected abstract KerberosRule getKerberosRule();
protected abstract CommonKerberosConfig getKerberosConfig(); protected abstract CommonKerberosConfig getKerberosConfig();
protected abstract ComponentRepresentation getUserStorageConfiguration(); protected abstract ComponentRepresentation getUserStorageConfiguration();
protected abstract void setKrb5ConfPath();
protected abstract boolean isStartEmbeddedLdapServer(); protected ComponentRepresentation getUserStorageConfiguration(String providerName, String providerId) {
Map<String,String> kerberosConfig = getKerberosRule().getConfig();
MultivaluedHashMap<String, String> config = toComponentConfig(kerberosConfig);
UserStorageProviderModel model = new UserStorageProviderModel();
model.setLastSync(0);
model.setChangedSyncPeriod(-1);
model.setFullSyncPeriod(-1);
model.setName(providerName);
model.setPriority(0);
model.setProviderId(providerId);
model.setConfig(config);
ComponentRepresentation rep = ModelToRepresentation.toRepresentationWithoutConfig(model);
return rep;
}
@Override @Override
public void addTestRealms(List<RealmRepresentation> testRealms) { public void addTestRealms(List<RealmRepresentation> testRealms) {
@ -118,6 +129,11 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
testRealms.add(realmRep); testRealms.add(realmRep);
} }
@Override
public RealmResource testRealmResource() {
return adminClient.realm("test");
}
@Before @Before
@Override @Override
@ -127,7 +143,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
testRealmPage.setAuthRealm(AuthRealm.TEST); testRealmPage.setAuthRealm(AuthRealm.TEST);
changePasswordPage.realm(AuthRealm.TEST); changePasswordPage.realm(AuthRealm.TEST);
setKrb5ConfPath(); getKerberosRule().setKrb5ConfPath(testingClient.testing());
spnegoSchemeFactory = new KeycloakSPNegoSchemeFactory(getKerberosConfig()); spnegoSchemeFactory = new KeycloakSPNegoSchemeFactory(getKerberosConfig());
initHttpClient(true); initHttpClient(true);
@ -161,164 +177,31 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
// } // }
@Test protected AccessToken assertSuccessfulSpnegoLogin(String loginUsername, String expectedUsername, String password) throws Exception {
public void spnegoNotAvailableTest() throws Exception { Response spnegoResponse = spnegoLogin(loginUsername, password);
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();
}
// KEYCLOAK-7823
@Test
public void spnegoLoginWithRequiredKerberosAuthExecutionTest() {
AuthenticationExecutionModel.Requirement oldRequirement = updateKerberosAuthExecutionRequirement(
AuthenticationExecutionModel.Requirement.REQUIRED);
Response response = spnegoLogin("hnelson", "secret");
updateKerberosAuthExecutionRequirement(oldRequirement);
Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
}
protected OAuthClient.AccessTokenResponse spnegoLoginTestImpl() throws Exception {
Response spnegoResponse = spnegoLogin("hnelson", "secret");
Assert.assertEquals(302, spnegoResponse.getStatus()); Assert.assertEquals(302, spnegoResponse.getStatus());
List<UserRepresentation> users = testRealmResource().users().search("hnelson", 0, 1); List<UserRepresentation> users = testRealmResource().users().search(expectedUsername, 0, 1);
String userId = users.get(0).getId(); String userId = users.get(0).getId();
events.expectLogin() events.expectLogin()
.client("kerberos-app") .client("kerberos-app")
.user(userId) .user(userId)
.detail(Details.USERNAME, "hnelson") .detail(Details.USERNAME, expectedUsername)
.assertEvent(); .assertEvent();
String codeUrl = spnegoResponse.getLocation().toString(); String codeUrl = spnegoResponse.getLocation().toString();
return assertAuthenticationSuccess(codeUrl); OAuthClient.AccessTokenResponse tokenResponse = assertAuthenticationSuccess(codeUrl);
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
Assert.assertEquals(userId, token.getSubject());
Assert.assertEquals(expectedUsername, token.getPreferredUsername());
return token;
} }
protected abstract boolean isCaseSensitiveLogin(); protected String invokeLdap(GSSCredential gssCredential, String username) throws NamingException {
// 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, 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); Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:10389"); env.put(Context.PROVIDER_URL, "ldap://localhost:10389");
@ -462,7 +345,8 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider); testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider);
} }
private AuthenticationExecutionModel.Requirement updateKerberosAuthExecutionRequirement(AuthenticationExecutionModel.Requirement requirement) {
protected AuthenticationExecutionModel.Requirement updateKerberosAuthExecutionRequirement(AuthenticationExecutionModel.Requirement requirement) {
Optional<AuthenticationExecutionInfoRepresentation> kerberosAuthExecutionOpt = testRealmResource() Optional<AuthenticationExecutionInfoRepresentation> kerberosAuthExecutionOpt = testRealmResource()
.flows() .flows()
.getExecutions(DefaultAuthenticationFlows.BROWSER_FLOW) .getExecutions(DefaultAuthenticationFlows.BROWSER_FLOW)
@ -483,15 +367,9 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
return oldRequirement; return oldRequirement;
} }
@Override
public RealmResource testRealmResource() {
return adminClient.realm("test");
}
// TODO: Use LDAPTestUtils.toComponentConfig once it's migrated to new testsuite private static MultivaluedHashMap<String, String> toComponentConfig(Map<String, String> ldapConfig) {
public static MultivaluedHashMap<String, String> toComponentConfig(Map<String, String> ldapConfig) {
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>(); MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
for (Map.Entry<String, String> entry : ldapConfig.entrySet()) { for (Map.Entry<String, String> entry : ldapConfig.entrySet()) {
config.add(entry.getKey(), entry.getValue()); config.add(entry.getKey(), entry.getValue());

View file

@ -0,0 +1,117 @@
/*
* Copyright 2017 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 javax.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.TargetsContainer;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.federation.ldap.LDAPTestContext;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.KerberosRule;
import org.keycloak.testsuite.util.LDAPTestUtils;
import org.keycloak.util.ldap.KerberosEmbeddedServer;
import org.keycloak.util.ldap.LDAPEmbeddedServer;
import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class KerberosLdapCrossRealmTrustTest extends AbstractKerberosTest {
@Deployment
@TargetsContainer(AUTH_SERVER_CURRENT)
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class, LDAPEmbeddedServer.class, KerberosEmbeddedServer.class)
.addPackages(true,
"org.keycloak.testsuite",
"org.keycloak.testsuite.federation.ldap",
"org.keycloak.testsuite.federation.kerberos");
}
private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-ldap-crt-connection.properties";
@ClassRule
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
@ClassRule
public static KerberosRule kerberosRule2 = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM_2);
@Override
protected KerberosRule getKerberosRule() {
return kerberosRule;
}
@Override
protected CommonKerberosConfig getKerberosConfig() {
return new LDAPProviderKerberosConfig(getUserStorageConfiguration());
}
@Override
protected ComponentRepresentation getUserStorageConfiguration() {
return getUserStorageConfiguration("kerberos-ldap", LDAPStorageProviderFactory.PROVIDER_NAME);
}
@Test
public void test01SpnegoLoginCRTSuccess() throws Exception {
// Login as user from realm KC2.COM . Realm KEYCLOAK.ORG will trust us
AccessToken token = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret");
Assert.assertEquals(token.getEmail(), "hnelson2@kc2.com");
assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", false);
}
@Test
public void test02DisableTrust() throws Exception {
// Remove the LDAP entry corresponding to the Kerberos principal krbtgt/KEYCLOAK.ORG@KC2.COM
// This will effectively disable kerberos cross-realm trust
testingClient.testing().ldap("test").removeLDAPUser("krbtgt2");
// There is no trust among kerberos realms anymore. SPNEGO shouldn't work. There would be failure even on Apache HTTP client side
// as it's not possible to start GSS context ( initSecContext ) due the missing trust among realms.
try {
Response spnegoResponse = spnegoLogin("hnelson2@KC2.COM", "secret");
Assert.fail("Not expected to successfully login");
} catch (Exception e) {
// Expected
}
}
}

View file

@ -17,40 +17,41 @@
package org.keycloak.testsuite.federation.kerberos; package org.keycloak.testsuite.federation.kerberos;
import java.io.File;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.apache.commons.io.FileUtils;
import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
import org.junit.Assert; import org.junit.Assert;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory; import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.testsuite.util.KerberosRule; import org.keycloak.testsuite.util.KerberosRule;
import org.keycloak.util.ldap.KerberosEmbeddedServer;
/** /**
* Test for the LDAPStorageProvider with kerberos enabled (kerberos with LDAP integration)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class KerberosLdapTest extends AbstractKerberosTest { public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-ldap-connection.properties"; private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-ldap-connection.properties";
@ClassRule @ClassRule
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION); public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
@Override
protected KerberosRule getKerberosRule() {
return kerberosRule;
}
@Override @Override
protected CommonKerberosConfig getKerberosConfig() { protected CommonKerberosConfig getKerberosConfig() {
@ -59,43 +60,14 @@ public class KerberosLdapTest extends AbstractKerberosTest {
@Override @Override
protected ComponentRepresentation getUserStorageConfiguration() { protected ComponentRepresentation getUserStorageConfiguration() {
Map<String,String> kerberosConfig = kerberosRule.getConfig(); return getUserStorageConfiguration("kerberos-ldap", LDAPStorageProviderFactory.PROVIDER_NAME);
MultivaluedHashMap<String, String> config = toComponentConfig(kerberosConfig);
UserStorageProviderModel model = new UserStorageProviderModel();
model.setLastSync(0);
model.setChangedSyncPeriod(-1);
model.setFullSyncPeriod(-1);
model.setName("kerberos-ldap");
model.setPriority(0);
model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
model.setConfig(config);
ComponentRepresentation rep = ModelToRepresentation.toRepresentationWithoutConfig(model);
return rep;
} }
@Override
protected boolean isCaseSensitiveLogin() {
return kerberosRule.isCaseSensitiveLogin();
}
@Override
protected boolean isStartEmbeddedLdapServer() {
return kerberosRule.isStartEmbeddedLdapServer();
}
@Override
protected void setKrb5ConfPath() {
kerberosRule.setKrb5ConfPath(testingClient.testing());
}
@Test @Test
public void spnegoLoginTest() throws Exception { public void spnegoLoginTest() throws Exception {
spnegoLoginTestImpl(); assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret");
// Assert user was imported and hasn't any required action on him. Profile info is synced from LDAP // Assert user was imported and hasn't any required action on him. Profile info is synced from LDAP
assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false); assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false);

View file

@ -0,0 +1,100 @@
/*
* Copyright 2017 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 org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.TargetsContainer;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.federation.kerberos.KerberosConfig;
import org.keycloak.federation.kerberos.KerberosFederationProviderFactory;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.testsuite.federation.ldap.AbstractLDAPTest;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.KerberosRule;
import org.keycloak.util.ldap.KerberosEmbeddedServer;
import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class KerberosStandaloneCrossRealmTrustTest extends AbstractKerberosTest {
@Deployment
@TargetsContainer(AUTH_SERVER_CURRENT)
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class, AbstractLDAPTest.class, AbstractKerberosTest.class)
.addPackages(true,
"org.keycloak.testsuite",
"org.keycloak.testsuite.federation.ldap",
"org.keycloak.testsuite.federation.kerberos");
}
private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-standalone-connection.properties";
@ClassRule
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
@ClassRule
public static KerberosRule kerberosRule2 = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM_2);
@Override
protected KerberosRule getKerberosRule() {
return kerberosRule;
}
@Override
protected CommonKerberosConfig getKerberosConfig() {
return new KerberosConfig(getUserStorageConfiguration());
}
@Override
protected ComponentRepresentation getUserStorageConfiguration() {
return getUserStorageConfiguration("kerberos-standalone", KerberosFederationProviderFactory.PROVIDER_NAME);
}
@Test
public void test01spnegoLoginSameRealmTest() throws Exception {
assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret");
assertUser("hnelson", "hnelson@keycloak.org", null, null, false);
}
@Test
public void test02spnegoLoginDifferentRealmTest() throws Exception {
// Cross-realm trust login. Realm KEYCLOAK.ORG trusts realm KC2.COM.
// TODO: email hnelson2@keycloak.org is not very good. Will be better to have more flexibility for mapping of kerberos principals to Keycloak UserModel in KerberosFederationProvider (if needed)
assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret");
assertUser("hnelson2", "hnelson2@keycloak.org", null, null, false);
}
}

View file

@ -18,11 +18,7 @@
package org.keycloak.testsuite.federation.kerberos; package org.keycloak.testsuite.federation.kerberos;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.client.Entity; import javax.ws.rs.client.Entity;
@ -33,28 +29,34 @@ import org.junit.Assert;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.constants.KerberosConstants; import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.KerberosConfig;
import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; import org.keycloak.federation.kerberos.KerberosFederationProviderFactory;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.ActionURIUtils;
import org.keycloak.testsuite.util.KerberosRule; import org.keycloak.testsuite.util.KerberosRule;
import org.keycloak.util.ldap.KerberosEmbeddedServer;
/** /**
* Test for the KerberosFederationProvider (kerberos without LDAP integration)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class KerberosStandaloneTest extends AbstractKerberosTest { public class KerberosStandaloneTest extends AbstractKerberosSingleRealmTest {
private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-standalone-connection.properties"; private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-standalone-connection.properties";
@ClassRule @ClassRule
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION); public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
@Override
protected KerberosRule getKerberosRule() {
return kerberosRule;
}
@Override @Override
protected CommonKerberosConfig getKerberosConfig() { protected CommonKerberosConfig getKerberosConfig() {
@ -63,44 +65,15 @@ public class KerberosStandaloneTest extends AbstractKerberosTest {
@Override @Override
protected ComponentRepresentation getUserStorageConfiguration() { protected ComponentRepresentation getUserStorageConfiguration() {
Map<String,String> kerberosConfig = kerberosRule.getConfig(); return getUserStorageConfiguration("kerberos-standalone", KerberosFederationProviderFactory.PROVIDER_NAME);
MultivaluedHashMap<String, String> config = toComponentConfig(kerberosConfig);
UserStorageProviderModel model = new UserStorageProviderModel();
model.setLastSync(0);
model.setChangedSyncPeriod(-1);
model.setFullSyncPeriod(-1);
model.setName("kerberos-standalone");
model.setPriority(0);
model.setProviderId(KerberosFederationProviderFactory.PROVIDER_NAME);
model.setConfig(config);
ComponentRepresentation rep = ModelToRepresentation.toRepresentationWithoutConfig(model);
return rep;
} }
@Override
protected boolean isCaseSensitiveLogin() {
return kerberosRule.isCaseSensitiveLogin();
}
@Override
protected boolean isStartEmbeddedLdapServer() {
return kerberosRule.isStartEmbeddedLdapServer();
}
@Override
protected void setKrb5ConfPath() {
kerberosRule.setKrb5ConfPath(testingClient.testing());
}
@Test @Test
public void spnegoLoginTest() throws Exception { public void spnegoLoginTest() throws Exception {
spnegoLoginTestImpl(); assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret");
// Assert user was imported and hasn't any required action on him. Profile info is synced from LDAP // Assert user was imported and hasn't any required action on him. Profile info is NOT synced from LDAP. Just username is filled and email is "guessed"
assertUser("hnelson", "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM).toLowerCase(), null, null, false); assertUser("hnelson", "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM).toLowerCase(), null, null, false);
} }

View file

@ -31,6 +31,8 @@ import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
/** /**
@ -74,7 +76,19 @@ public class KeycloakSPNegoSchemeFactory extends SPNegoSchemeFactory {
@Override @Override
protected byte[] generateGSSToken(byte[] input, Oid oid, String authServer, Credentials credentials) throws GSSException { protected byte[] generateGSSToken(byte[] input, Oid oid, String authServer, Credentials credentials) throws GSSException {
KerberosUsernamePasswordAuthenticator authenticator = new KerberosUsernamePasswordAuthenticator(kerberosConfig); KerberosUsernamePasswordAuthenticator authenticator = new KerberosUsernamePasswordAuthenticator(kerberosConfig) {
// Disable strict check for the configured kerberos realm, which is on super-method
@Override
protected String getKerberosPrincipal(String username) throws LoginException {
if (username.contains("@")) {
return username;
} else {
return username + "@" + config.getKerberosRealm();
}
}
};
try { try {
Subject clientSubject = authenticator.authenticateSubject(username, password); Subject clientSubject = authenticator.authenticateSubject(username, password);
@ -109,7 +123,8 @@ public class KeycloakSPNegoSchemeFactory extends SPNegoSchemeFactory {
token = new byte[0]; token = new byte[0];
} }
GSSManager manager = getManager(); GSSManager manager = getManager();
GSSName serverName = manager.createName("HTTP/" + authServer + "@" + kerberosConfig.getKerberosRealm(), null); String httPrincipal = kerberosConfig.getServerPrincipal().replaceFirst("/.*@", "/" + authServer + "@");
GSSName serverName = manager.createName(httPrincipal, null);
GSSContext gssContext = manager.createContext( GSSContext gssContext = manager.createContext(
serverName.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME); serverName.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true); gssContext.requestMutualAuth(true);

View file

@ -24,20 +24,12 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.FixMethodOrder; import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runners.MethodSorters; import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory; import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig; import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;

View file

@ -0,0 +1,36 @@
#
# Copyright 2017 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.
#
# Using LDAP from "kc2" domain, but HTTP principal is configured to use KEYCLOAK.ORG domain.
# Realm KC2.COM has the purpose just for the client side login (KerberosUsernamePasswordAuthenticator used by Apache HTTP client in the test)
idm.test.ldap.connection.url=ldap\://localhost\:11389
idm.test.ldap.base.dn=dc\=kc2,dc\=com
idm.test.ldap.roles.dn.suffix=ou\=Roles,dc\=kc2,dc\=com
idm.test.ldap.group.dn.suffix=ou\=Groups,dc\=kc2,dc\=com
idm.test.ldap.user.dn.suffix=ou\=People,dc\=kc2,dc\=com
idm.test.ldap.start.embedded.ldap.server=true
idm.test.ldap.bind.dn=uid\=admin,ou\=system
idm.test.ldap.bind.credential=secret
idm.test.ldap.connection.pooling=true
idm.test.ldap.pagination=true
idm.test.ldap.batch.size.for.sync=3
idm.test.kerberos.allow.kerberos.authentication=true
idm.test.kerberos.realm=KC2.COM
idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG
idm.test.kerberos.debug=true
idm.test.kerberos.use.kerberos.for.password.authentication=true

View file

@ -14,6 +14,9 @@
KEYCLOAK.ORG = { KEYCLOAK.ORG = {
kdc = localhost:6088 kdc = localhost:6088
} }
KC2.COM = {
kdc = localhost:7088
}
[domain_realm] [domain_realm]
localhost = KEYCLOAK.ORG localhost = KEYCLOAK.ORG

View file

@ -0,0 +1,104 @@
dn: dc=kc2,dc=com
objectclass: dcObject
objectclass: organization
o: Kc2
dc: Kc2
dn: ou=People,dc=kc2,dc=com
objectClass: organizationalUnit
objectClass: top
ou: People
dn: uid=krbtgt,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt
userPassword: secret
krb5PrincipalName: krbtgt/KC2.COM@KC2.COM
krb5KeyVersionNumber: 0
# Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM
dn: uid=krbtgt2,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt2
userPassword: secret
krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=ldap,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: LDAP
sn: Service
uid: ldap
userPassword: randall
krb5PrincipalName: ${ldapSaslPrincipal}
krb5KeyVersionNumber: 0
dn: uid=HTTP,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: HTTP
sn: Service
uid: HTTP
userPassword: httppwd
krb5PrincipalName: HTTP/${hostname}@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=hnelson2,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: Horatio
sn: Nelson
mail: hnelson2@kc2.com
uid: hnelson2
userPassword: secret
krb5PrincipalName: hnelson2@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=jduke2,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: Java
sn: Duke
mail: jduke2@keycloak.org
uid: jduke2
userPassword: theduke
krb5PrincipalName: jduke2@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=gsstestserver,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: gsstestserver
sn: Service
uid: gsstestserver
userPassword: gsstestpwd
krb5PrincipalName: gsstestserver/xxx@KC2.COM
krb5KeyVersionNumber: 0

View file

@ -22,6 +22,20 @@ userPassword: secret
krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG
krb5KeyVersionNumber: 0 krb5KeyVersionNumber: 0
# Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM
dn: uid=krbtgt2,ou=People,dc=keycloak,dc=org
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt2
userPassword: secret
krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=ldap,ou=People,dc=keycloak,dc=org dn: uid=ldap,ou=People,dc=keycloak,dc=org
objectClass: top objectClass: top
objectClass: person objectClass: person

View file

@ -10,6 +10,7 @@
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-model-infinispan"/> <module name="org.keycloak.keycloak-model-infinispan"/>
<module name="org.keycloak.keycloak-model-jpa"/> <module name="org.keycloak.keycloak-model-jpa"/>
<module name="org.keycloak.keycloak-kerberos-federation"/>
<module name="org.keycloak.keycloak-ldap-federation"/> <module name="org.keycloak.keycloak-ldap-federation"/>
</dependencies> </dependencies>
</deployment> </deployment>

View file

@ -43,6 +43,7 @@
<dependency> <dependency>
<groupId>log4j</groupId> <groupId>log4j</groupId>
<artifactId>log4j</artifactId> <artifactId>log4j</artifactId>
<scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View file

@ -106,7 +106,8 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
// EhCache in disabled-like-mode // EhCache in disabled-like-mode
Configuration ehCacheConfig = new Configuration(); Configuration ehCacheConfig = new Configuration();
CacheConfiguration defaultCache = new CacheConfiguration("default", 1).eternal(false).timeToIdleSeconds(30) ehCacheConfig.setName(name);
CacheConfiguration defaultCache = new CacheConfiguration(name + "-default", 1).eternal(false).timeToIdleSeconds(30)
.timeToLiveSeconds(30).overflowToDisk(false); .timeToLiveSeconds(30).overflowToDisk(false);
ehCacheConfig.addDefaultCache(defaultCache); ehCacheConfig.addDefaultCache(defaultCache);
CacheService cacheService = new CacheService(new CacheManager(ehCacheConfig)); CacheService cacheService = new CacheService(new CacheManager(ehCacheConfig));

View file

@ -60,7 +60,9 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
private static final String DEFAULT_KERBEROS_LDIF_FILE = "classpath:kerberos/default-users.ldif"; private static final String DEFAULT_KERBEROS_LDIF_FILE = "classpath:kerberos/default-users.ldif";
private static final String DEFAULT_KERBEROS_REALM = "KEYCLOAK.ORG"; public static final String DEFAULT_KERBEROS_REALM = "KEYCLOAK.ORG";
public static final String DEFAULT_KERBEROS_REALM_2 = "KC2.COM";
private static final String DEFAULT_KDC_PORT = "6088"; private static final String DEFAULT_KDC_PORT = "6088";
private static final String DEFAULT_KDC_ENCRYPTION_TYPES = "aes128-cts-hmac-sha1-96, des-cbc-md5, des3-cbc-sha1-kd"; private static final String DEFAULT_KDC_ENCRYPTION_TYPES = "aes128-cts-hmac-sha1-96, des-cbc-md5, des3-cbc-sha1-kd";
@ -75,9 +77,31 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
Properties defaultProperties = new Properties(); Properties defaultProperties = new Properties();
defaultProperties.put(PROPERTY_DSF, DSF_FILE); defaultProperties.put(PROPERTY_DSF, DSF_FILE);
String kerberosRealm = System.getProperty("keycloak.kerberos.realm", DEFAULT_KERBEROS_REALM);
configureDefaultPropertiesForRealm(kerberosRealm, defaultProperties);
execute(args, defaultProperties); execute(args, defaultProperties);
} }
public static void configureDefaultPropertiesForRealm(String kerberosRealm, Properties properties) {
log.infof("Using kerberos realm: %s", kerberosRealm);
if (DEFAULT_KERBEROS_REALM.equals(kerberosRealm)) {
// No more configs
} else if (DEFAULT_KERBEROS_REALM_2.equals(kerberosRealm)) {
properties.put(PROPERTY_BASE_DN, "dc=kc2,dc=com");
properties.put(PROPERTY_BIND_PORT, "11389");
properties.put(PROPERTY_BIND_LDAPS_PORT, "11636");
properties.put(PROPERTY_LDIF_FILE, "classpath:kerberos/default-users-kc2.ldif");
properties.put(PROPERTY_KERBEROS_REALM, DEFAULT_KERBEROS_REALM_2);
properties.put(PROPERTY_KDC_PORT, "7088");
} else {
throw new IllegalArgumentException("Valid values for kerberos realm are [ " + DEFAULT_KERBEROS_REALM + " , "
+ DEFAULT_KERBEROS_REALM_2 + " ]");
}
}
public static void execute(String[] args, Properties defaultProperties) throws Exception { public static void execute(String[] args, Properties defaultProperties) throws Exception {
final KerberosEmbeddedServer kerberosEmbeddedServer = new KerberosEmbeddedServer(defaultProperties); final KerberosEmbeddedServer kerberosEmbeddedServer = new KerberosEmbeddedServer(defaultProperties);
kerberosEmbeddedServer.init(); kerberosEmbeddedServer.init();

View file

@ -0,0 +1,104 @@
dn: dc=kc2,dc=com
objectclass: dcObject
objectclass: organization
o: Kc2
dc: Kc2
dn: ou=People,dc=kc2,dc=com
objectClass: organizationalUnit
objectClass: top
ou: People
dn: uid=krbtgt,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt
userPassword: secret
krb5PrincipalName: krbtgt/KC2.COM@KC2.COM
krb5KeyVersionNumber: 0
# Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM
dn: uid=krbtgt2,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt2
userPassword: secret
krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=ldap,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: LDAP
sn: Service
uid: ldap
userPassword: randall
krb5PrincipalName: ${ldapSaslPrincipal}
krb5KeyVersionNumber: 0
dn: uid=HTTP,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: HTTP
sn: Service
uid: HTTP
userPassword: httppwd
krb5PrincipalName: HTTP/${hostname}@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=hnelson2,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: Horatio
sn: Nelson
mail: hnelson2@kc2.com
uid: hnelson2
userPassword: secret
krb5PrincipalName: hnelson2@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=jduke2,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: Java
sn: Duke
mail: jduke2@keycloak.org
uid: jduke2
userPassword: theduke
krb5PrincipalName: jduke2@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=gsstestserver,ou=People,dc=kc2,dc=com
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: gsstestserver
sn: Service
uid: gsstestserver
userPassword: gsstestpwd
krb5PrincipalName: gsstestserver/xxx@KC2.COM
krb5KeyVersionNumber: 0

View file

@ -22,6 +22,20 @@ userPassword: secret
krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG
krb5KeyVersionNumber: 0 krb5KeyVersionNumber: 0
# Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM
dn: uid=krbtgt2,ou=People,dc=keycloak,dc=org
objectClass: top
objectClass: person
objectClass: inetOrgPerson
objectClass: krb5principal
objectClass: krb5kdcentry
cn: KDC Service
sn: Service
uid: krbtgt2
userPassword: secret
krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM
krb5KeyVersionNumber: 0
dn: uid=ldap,ou=People,dc=keycloak,dc=org dn: uid=ldap,ou=People,dc=keycloak,dc=org
objectClass: top objectClass: top
objectClass: person objectClass: person

View file

@ -23,4 +23,7 @@ log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
log4j.logger.org.keycloak=info log4j.logger.org.keycloak=info
log4j.logger.org.apache.directory.api=warn log4j.logger.org.apache.directory.api=warn
log4j.logger.org.apache.directory.server.core=warn log4j.logger.org.apache.directory.server.core=warn
# Enable to view detailed AS REQ and TGS REQ requests to embedded Kerberos server
#log4j.logger.org.apache.directory.server.kerberos=debug