diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index dcaa5fa011..4215948965 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -1,5 +1,6 @@ package org.keycloak.federation.ldap; +import org.jboss.logging.Logger; import org.keycloak.models.FederationProvider; import org.keycloak.models.FederationProviderModel; import org.keycloak.models.KeycloakSession; @@ -14,6 +15,7 @@ import org.picketlink.idm.IdentityManager; import org.picketlink.idm.PartitionManager; import org.picketlink.idm.credential.Credentials; import org.picketlink.idm.credential.Password; +import org.picketlink.idm.credential.TOTPCredential; import org.picketlink.idm.credential.UsernamePasswordCredentials; import org.picketlink.idm.model.basic.BasicModel; import org.picketlink.idm.model.basic.User; @@ -31,6 +33,8 @@ import static org.picketlink.idm.IDMMessages.MESSAGES; * @version $Revision: 1 $ */ public class LDAPFederationProvider implements FederationProvider { + private static final Logger logger = Logger.getLogger(LDAPFederationProvider.class); + protected KeycloakSession session; protected FederationProviderModel model; protected PartitionManager partitionManager; @@ -73,7 +77,7 @@ public class LDAPFederationProvider implements FederationProvider { @Override public UserModel proxy(UserModel local) { // todo - return local; + return new LDAPUserModelDelegate(local, this); } @Override @@ -177,11 +181,12 @@ public class LDAPFederationProvider implements FederationProvider { protected UserModel importUserFromPicketlink(RealmModel realm, User picketlinkUser) { String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null; UserModel imported = session.userStorage().addUser(realm, picketlinkUser.getLoginName()); + imported.setEnabled(true); imported.setEmail(email); imported.setFirstName(picketlinkUser.getFirstName()); imported.setLastName(picketlinkUser.getLastName()); imported.setFederationLink(model.getId()); - return imported; + return proxy(imported); } protected User queryByEmail(IdentityManager identityManager, String email) throws IdentityManagementException { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index ecc3b34ac7..6a0c57d64a 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentHashMap; * @version $Revision: 1 $ */ public class LDAPFederationProviderFactory implements FederationProviderFactory { + public static final String PROVIDER_NAME = "ldap"; PartitionManagerRegistry registry; @Override @@ -41,6 +42,6 @@ public class LDAPFederationProviderFactory implements FederationProviderFactory @Override public String getId() { - return "ldap"; + return PROVIDER_NAME; } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUserModelDelegate.java index a96c6bdedc..a2936d5d57 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUserModelDelegate.java @@ -1,5 +1,6 @@ package org.keycloak.federation.ldap; +import org.jboss.logging.Logger; import org.keycloak.models.ApplicationModel; import org.keycloak.models.AuthenticationLinkModel; import org.keycloak.models.FederationProviderModel; @@ -10,6 +11,8 @@ import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.picketlink.idm.IdentityManagementException; import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.credential.Password; +import org.picketlink.idm.credential.TOTPCredential; import org.picketlink.idm.model.basic.BasicModel; import org.picketlink.idm.model.basic.User; @@ -21,62 +24,83 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class LDAPUserModelDelegate { +public class LDAPUserModelDelegate implements UserModel { + private static final Logger logger = Logger.getLogger(LDAPUserModelDelegate.class); + protected UserModel delegate; protected LDAPFederationProvider provider; + public LDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider) { + this.delegate = delegate; + this.provider = provider; + } + + @Override public String getId() { return delegate.getId(); } + @Override public void setAttribute(String name, String value) { delegate.setAttribute(name, value); } + @Override public boolean isEmailVerified() { return delegate.isEmailVerified(); } + @Override public void removeAttribute(String name) { delegate.removeAttribute(name); } + @Override public String getLastName() { return delegate.getLastName(); } + @Override public void setFederationLink(String link) { delegate.setFederationLink(link); } + @Override public AuthenticationLinkModel getAuthenticationLink() { return delegate.getAuthenticationLink(); } + @Override public Map getAttributes() { return delegate.getAttributes(); } + @Override public boolean hasRole(RoleModel role) { return delegate.hasRole(role); } + @Override public void grantRole(RoleModel role) { delegate.grantRole(role); } + @Override public void setEnabled(boolean enabled) { delegate.setEnabled(enabled); } + @Override public void removeRequiredAction(UserModel.RequiredAction action) { delegate.removeRequiredAction(action); } + @Override public void deleteRoleMapping(RoleModel role) { delegate.deleteRoleMapping(role); } + @Override public void setUsername(String username) { IdentityManager identityManager = provider.getIdentityManager(); @@ -93,14 +117,17 @@ public class LDAPUserModelDelegate { delegate.setUsername(username); } + @Override public boolean isEnabled() { return delegate.isEnabled(); } + @Override public String getFirstName() { return delegate.getFirstName(); } + @Override public void setLastName(String lastName) { IdentityManager identityManager = provider.getIdentityManager(); @@ -117,14 +144,39 @@ public class LDAPUserModelDelegate { delegate.setLastName(lastName); } + @Override public void setEmailVerified(boolean verified) { delegate.setEmailVerified(verified); } + @Override public void updateCredential(UserCredentialModel cred) { - delegate.updateCredential(cred); + if (!provider.getSupportedCredentialTypes().contains(cred.getType())) { + delegate.updateCredential(cred); + return; + } + IdentityManager identityManager = provider.getIdentityManager(); + + try { + User picketlinkUser = BasicModel.getUser(identityManager, getUsername()); + if (picketlinkUser == null) { + logger.debugf("User '%s' doesn't exists. Skip password update", getUsername()); + throw new IllegalStateException("User doesn't exist in LDAP storage"); + } + if (cred.getType().equals(UserCredentialModel.PASSWORD)) { + identityManager.updateCredential(picketlinkUser, new Password(cred.getValue().toCharArray())); + } else if (cred.getType().equals(UserCredentialModel.TOTP)) { + TOTPCredential credential = new TOTPCredential(cred.getValue()); + credential.setDevice(cred.getDevice()); + identityManager.updateCredential(picketlinkUser, credential); + } + } catch (IdentityManagementException ie) { + throw new ModelException(ie); + } + } + @Override public void setEmail(String email) { IdentityManager identityManager = provider.getIdentityManager(); @@ -141,62 +193,77 @@ public class LDAPUserModelDelegate { delegate.setEmail(email); } + @Override public void addRequiredAction(UserModel.RequiredAction action) { delegate.addRequiredAction(action); } + @Override public List getCredentialsDirectly() { return delegate.getCredentialsDirectly(); } + @Override public boolean isTotp() { return delegate.isTotp(); } + @Override public void setFirstName(String firstName) { delegate.setFirstName(firstName); } + @Override public Set getRequiredActions() { return delegate.getRequiredActions(); } + @Override public String getEmail() { return delegate.getEmail(); } + @Override public void setTotp(boolean totp) { delegate.setTotp(totp); } + @Override public void setAuthenticationLink(AuthenticationLinkModel authenticationLink) { delegate.setAuthenticationLink(authenticationLink); } + @Override public String getUsername() { return delegate.getUsername(); } + @Override public String getFederationLink() { return delegate.getFederationLink(); } + @Override public Set getRealmRoleMappings() { return delegate.getRealmRoleMappings(); } + @Override public Set getRoleMappings() { return delegate.getRoleMappings(); } + @Override public Set getApplicationRoleMappings(ApplicationModel app) { return delegate.getApplicationRoleMappings(app); } + @Override public String getAttribute(String name) { return delegate.getAttribute(name); } + @Override public void updateCredentialDirectly(UserCredentialValueModel cred) { delegate.updateCredentialDirectly(cred); } diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.FederationProviderFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.FederationProviderFactory new file mode 100755 index 0000000000..b8921e99fb --- /dev/null +++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.FederationProviderFactory @@ -0,0 +1 @@ +org.keycloak.federation.ldap.LDAPFederationProviderFactory \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/FederationManager.java b/model/api/src/main/java/org/keycloak/models/FederationManager.java index a9978aa0c8..8e79ba1ff2 100755 --- a/model/api/src/main/java/org/keycloak/models/FederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/FederationManager.java @@ -96,7 +96,7 @@ public class FederationManager implements UserProvider { return user; } for (FederationProviderModel federation : realm.getFederationProviders()) { - FederationProvider fed = session.getProvider(FederationProvider.class, federation.getProviderName()); + FederationProvider fed = getFederationProvider(federation); user = fed.getUserById(id, realm); if (user != null) return user; } @@ -114,7 +114,7 @@ public class FederationManager implements UserProvider { return user; } for (FederationProviderModel federation : realm.getFederationProviders()) { - FederationProvider fed = session.getProvider(FederationProvider.class, federation.getProviderName()); + FederationProvider fed = getFederationProvider(federation); user = fed.getUserByUsername(username, realm); if (user != null) return user; } @@ -132,7 +132,7 @@ public class FederationManager implements UserProvider { return user; } for (FederationProviderModel federation : realm.getFederationProviders()) { - FederationProvider fed = session.getProvider(FederationProvider.class, federation.getProviderName()); + FederationProvider fed = getFederationProvider(federation); user = fed.getUserByEmail(email, realm); if (user != null) return user; } @@ -186,7 +186,7 @@ public class FederationManager implements UserProvider { return results; } List federationProviders = realm.getFederationProviders(); - for (int i = federationProviders.size(); i >= 0; i--) { + for (int i = federationProviders.size() - 1; i >= 0; i--) { FederationProviderModel federation = federationProviders.get(i); FederationProvider fed = getFederationProvider(federation); query = fed.getUsers(realm, firstResult, maxResults); @@ -220,7 +220,7 @@ public class FederationManager implements UserProvider { return results; } List federationProviders = realm.getFederationProviders(); - for (int i = federationProviders.size(); i >= 0; i--) { + for (int i = federationProviders.size() - 1; i >= 0; i--) { FederationProviderModel federation = federationProviders.get(i); FederationProvider fed = getFederationProvider(federation); query = fed.searchForUser(search, realm, firstResult, maxResults); @@ -254,7 +254,7 @@ public class FederationManager implements UserProvider { return results; } List federationProviders = realm.getFederationProviders(); - for (int i = federationProviders.size(); i >= 0; i--) { + for (int i = federationProviders.size() - 1; i >= 0; i--) { FederationProviderModel federation = federationProviders.get(i); FederationProvider fed = getFederationProvider(federation); query = fed.searchForUserByAttributes(attributes, realm, firstResult, maxResults); @@ -286,7 +286,7 @@ public class FederationManager implements UserProvider { @Override public void preRemove(RealmModel realm) { for (FederationProviderModel federation : realm.getFederationProviders()) { - FederationProvider fed = session.getProvider(FederationProvider.class, federation.getProviderName()); + FederationProvider fed = getFederationProvider(federation); fed.preRemove(realm); } session.userStorage().preRemove(realm); @@ -295,7 +295,7 @@ public class FederationManager implements UserProvider { @Override public void preRemove(RealmModel realm, RoleModel role) { for (FederationProviderModel federation : realm.getFederationProviders()) { - FederationProvider fed = session.getProvider(FederationProvider.class, federation.getProviderName()); + FederationProvider fed = getFederationProvider(federation); fed.preRemove(realm, role); } session.userStorage().preRemove(realm, role); @@ -328,11 +328,12 @@ public class FederationManager implements UserProvider { public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { FederationProvider link = getFederationLink(realm, user); if (link != null) { - if (link.getSupportedCredentialTypes().size() > 0) { + Set supportedCredentialTypes = link.getSupportedCredentialTypes(); + if (supportedCredentialTypes.size() > 0) { List fedCreds = new ArrayList(); List localCreds = new ArrayList(); for (UserCredentialModel cred : input) { - if (fedCreds.contains(cred.getType())) { + if (supportedCredentialTypes.contains(cred.getType())) { fedCreds.add(cred); } else { localCreds.add(cred); diff --git a/model/api/src/main/java/org/keycloak/models/FederationSpi.java b/model/api/src/main/java/org/keycloak/models/FederationSpi.java new file mode 100755 index 0000000000..c427ec924d --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/FederationSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.models; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class FederationSpi implements Spi { + + @Override + public String getName() { + return "federation"; + } + + @Override + public Class getProviderClass() { + return FederationProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return FederationProviderFactory.class; + } + +} diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 706ba92934..58ccc7bbf0 100755 --- a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -1,3 +1,4 @@ +org.keycloak.models.FederationSpi org.keycloak.models.RealmSpi org.keycloak.models.UserSessionSpi org.keycloak.models.UserSpi \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 4d71501bcc..6b972ceaba 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -255,6 +255,7 @@ public class AuthenticationManager { protected AuthenticationStatus authenticateInternal(KeycloakSession session, RealmModel realm, MultivaluedMap formData, String username) { UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username); + if (user == null) { AuthUser authUser = AuthenticationProviderManager.getManager(realm, session).getUser(username); if (authUser != null) { diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 06ae32c50f..3aa1ffdaeb 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -97,6 +97,11 @@ httpclient ${keycloak.apache.httpcomponents.version} + + org.keycloak + keycloak-ldap-federation + ${project.version} + org.keycloak keycloak-undertow-adapter diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java new file mode 100755 index 0000000000..a2d03050f5 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java @@ -0,0 +1,183 @@ +package org.keycloak.testsuite.forms; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthProviderConstants; +import org.keycloak.federation.ldap.LDAPFederationProviderFactory; +import org.keycloak.model.test.LDAPEmbeddedServer; +import org.keycloak.model.test.LDAPTestUtils; +import org.keycloak.models.AuthenticationProviderModel; +import org.keycloak.models.FederationProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.AccountPasswordPage; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.RegisterPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class FederationProvidersIntegrationTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + addUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app"); + addUser(manager.getSession(), adminstrationRealm, "mary-admin", "mary@admin.com", "password-admin"); + + LDAPEmbeddedServer ldapServer = ldapRule.getEmbeddedServer(); + Map ldapConfig = new HashMap(); + ldapConfig.put(LDAPConstants.CONNECTION_URL, ldapServer.getConnectionUrl()); + ldapConfig.put(LDAPConstants.BASE_DN, ldapServer.getBaseDn()); + ldapConfig.put(LDAPConstants.BIND_DN, ldapServer.getBindDn()); + ldapConfig.put(LDAPConstants.BIND_CREDENTIAL, ldapServer.getBindCredential()); + ldapConfig.put(LDAPConstants.USER_DN_SUFFIX, ldapServer.getUserDnSuffix()); + ldapConfig.put(LDAPConstants.VENDOR, ldapServer.getVendor()); + + + FederationProviderModel ldapProvider = new FederationProviderModel(null, LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig); + appRealm.setFederationProviders(Arrays.asList(ldapProvider)); + + // Configure LDAP + ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm); + LDAPTestUtils.setLdapPassword(session, appRealm, "johnkeycloak", "password"); + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected WebDriver driver; + + @WebResource + protected AppPage appPage; + + @WebResource + protected RegisterPage registerPage; + + @WebResource + protected LoginPage loginPage; + + @WebResource + protected AccountUpdateProfilePage profilePage; + + @WebResource + protected AccountPasswordPage changePasswordPage; + + private static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) { + UserModel user = session.users().addUser(realm, username); + user.setEmail(email); + user.setEnabled(true); + + UserCredentialModel creds = new UserCredentialModel(); + creds.setType(CredentialRepresentation.PASSWORD); + creds.setValue(password); + + user.updateCredential(creds); + return user; + } + + @Test + public void loginClassic() { + loginPage.open(); + loginPage.login("mary", "password-app"); + + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + } + + @Test + public void loginLdap() { + loginPage.open(); + loginPage.login("johnkeycloak", "password"); + + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + profilePage.open(); + Assert.assertEquals("John", profilePage.getFirstName()); + Assert.assertEquals("Doe", profilePage.getLastName()); + Assert.assertEquals("john@email.org", profilePage.getEmail()); + } + + @Test + public void passwordChangeLdap() throws Exception { + changePasswordPage.open(); + loginPage.login("johnkeycloak", "password"); + changePasswordPage.changePassword("password", "new-password", "new-password"); + + Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); + + changePasswordPage.logout(); + + loginPage.open(); + loginPage.login("johnkeycloak", "password"); + Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + loginPage.open(); + loginPage.login("johnkeycloak", "new-password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } + + @Test + public void registerExistingLdapUser() { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.register("firstName", "lastName", "email", "existing", "password", "password"); + + registerPage.assertCurrent(); + Assert.assertEquals("Username already exists", registerPage.getError()); + } + + @Test + public void registerUserLdapSuccess() { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.register("firstName", "lastName", "email2", "registerUserSuccess2", "password", "password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } +}