diff --git a/forms/account-api/src/main/java/org/keycloak/account/Account.java b/forms/account-api/src/main/java/org/keycloak/account/Account.java index f92b161ecb..5a62fecbea 100644 --- a/forms/account-api/src/main/java/org/keycloak/account/Account.java +++ b/forms/account-api/src/main/java/org/keycloak/account/Account.java @@ -31,5 +31,5 @@ public interface Account { Account setEvents(List events); - Account setFeatures(boolean social, boolean audit); + Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported); } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java index 53f43d4593..9b6f0d81bd 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java @@ -44,6 +44,7 @@ public class FreeMarkerAccount implements Account { private List events; private boolean social; private boolean audit; + private boolean passwordUpdateSupported; public static enum MessageType {SUCCESS, WARNING, ERROR} @@ -95,7 +96,7 @@ public class FreeMarkerAccount implements Account { attributes.put("url", new UrlBean(realm, theme, baseUri)); - attributes.put("features", new FeaturesBean(social, audit)); + attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported)); switch (page) { case ACCOUNT: @@ -172,9 +173,10 @@ public class FreeMarkerAccount implements Account { } @Override - public Account setFeatures(boolean social, boolean audit) { + public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) { this.social = social; this.audit = audit; + this.passwordUpdateSupported = passwordUpdateSupported; return this; } } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java index 6f8158ba81..06e99eb001 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java @@ -7,10 +7,12 @@ public class FeaturesBean { private final boolean social; private final boolean log; + private final boolean passwordUpdateSupported; - public FeaturesBean(boolean social, boolean log) { + public FeaturesBean(boolean social, boolean log, boolean passwordUpdateSupported) { this.social = social; this.log = log; + this.passwordUpdateSupported = passwordUpdateSupported; } public boolean isSocial() { @@ -21,4 +23,7 @@ public class FeaturesBean { return log; } + public boolean isPasswordUpdateSupported() { + return passwordUpdateSupported; + } } diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl index 6a39817a40..883d8fbb26 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/template.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl @@ -40,7 +40,7 @@
  • Account
  • -
  • Password
  • + <#if features.passwordUpdateSupported>
  • Password
  • Authenticator
  • <#if features.social>
  • Social
  • <#if features.log>
  • Log
  • 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 8bb9e53be4..2d942d9b56 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -231,6 +231,7 @@ public class AuthenticationManager { user.setLastName(authUser.getLastName()); user.setEmail(authUser.getEmail()); realm.setAuthenticationLink(user, new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId())); + logger.info("User " + authUser.getUsername() + " created and linked with provider " + authUser.getProviderName()); } else { logger.warn("User " + username + " not found"); return AuthenticationStatus.INVALID_USER; diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index a7896600a3..bdc97fea8a 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -35,6 +35,8 @@ import org.keycloak.audit.Events; import org.keycloak.jaxrs.JaxrsOAuthClient; import org.keycloak.models.AccountRoles; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.AuthenticationLinkModel; +import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; @@ -143,12 +145,21 @@ public class AccountService { public void init() { auditProvider = providers.getProvider(AuditProvider.class); - account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setFeatures(realm.isSocial(), auditProvider != null); + account = AccountLoader.load().createAccount(uriInfo).setRealm(realm); + boolean passwordUpdateSupported = false; auth = authManager.authenticate(realm, headers); if (auth != null) { account.setUser(auth.getUser()); + + AuthenticationLinkModel authLinkModel = realm.getAuthenticationLink(auth.getUser()); + if (authLinkModel != null) { + AuthenticationProviderModel authProviderModel = AuthenticationProviderManager.getConfiguredProviderModel(realm, authLinkModel.getAuthProvider()); + passwordUpdateSupported = authProviderModel.isPasswordUpdateSupported(); + } } + + account.setFeatures(realm.isSocial(), auditProvider != null, passwordUpdateSupported); } public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) { diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index b326216889..a18e141068 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -383,13 +383,15 @@ public class TokenService { return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration(); } - UserModel user = realm.getUser(username); - if (user != null) { + AuthenticationProviderManager authenticationProviderManager = AuthenticationProviderManager.getManager(realm); + + // Validate that user with this username doesn't exist in realm or any authentication provider + if (realm.getUser(username) != null || authenticationProviderManager.getUser(username) != null) { audit.error(Errors.USERNAME_IN_USE); return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); } - user = realm.addUser(username); + UserModel user = realm.addUser(username); user.setEnabled(true); user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java index 66166ab0e0..d69c4284bd 100644 --- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java +++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java @@ -29,6 +29,14 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat return user == null ? null : createAuthenticatedUserInstance(user); } + @Override + public String registerUser(RealmModel currentRealm, Map config, String username) throws AuthenticationProviderException { + RealmModel realm = getRealm(currentRealm, config); + UserModel user = currentRealm.addUser(username); + user.setEnabled(true); + return user.getId(); + } + @Override public AuthProviderStatus validatePassword(RealmModel currentRealm, Map config, String username, String password) throws AuthenticationProviderException { RealmModel realm = getRealm(currentRealm, config); diff --git a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java index 4111806eca..8344425159 100755 --- a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java +++ b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java @@ -14,6 +14,7 @@ import org.keycloak.spi.authentication.AuthenticationProvider; import org.keycloak.spi.authentication.AuthenticationProviderException; import org.keycloak.spi.picketlink.PartitionManagerProvider; import org.keycloak.util.ProviderLoader; +import org.picketlink.idm.IdentityManagementException; import org.picketlink.idm.IdentityManager; import org.picketlink.idm.PartitionManager; import org.picketlink.idm.credential.Credentials; @@ -44,25 +45,47 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider @Override public AuthUser getUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException { IdentityManager identityManager = getIdentityManager(realm); - User picketlinkUser = BasicModel.getUser(identityManager, username); - return picketlinkUser == null ? null : new AuthUser(picketlinkUser.getId(), picketlinkUser.getLoginName(), getName()) - .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) - .setEmail(picketlinkUser.getEmail()) - .setProviderName(getName()); + + try { + User picketlinkUser = BasicModel.getUser(identityManager, username); + return picketlinkUser == null ? null : new AuthUser(picketlinkUser.getId(), picketlinkUser.getLoginName(), getName()) + .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) + .setEmail(picketlinkUser.getEmail()) + .setProviderName(getName()); + } catch (IdentityManagementException ie) { + throw convertIDMException(ie); + } + } + + @Override + public String registerUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException { + IdentityManager identityManager = getIdentityManager(realm); + + try { + User picketlinkUser = new User(username); + identityManager.add(picketlinkUser); + return picketlinkUser.getId(); + } catch (IdentityManagementException ie) { + throw convertIDMException(ie); + } } @Override public AuthProviderStatus validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { IdentityManager identityManager = getIdentityManager(realm); - UsernamePasswordCredentials credential = new UsernamePasswordCredentials(); - credential.setUsername(username); - credential.setPassword(new Password(password.toCharArray())); - identityManager.validateCredentials(credential); - if (credential.getStatus() == Credentials.Status.VALID) { - return AuthProviderStatus.SUCCESS; - } else { - return AuthProviderStatus.INVALID_CREDENTIALS; + try { + UsernamePasswordCredentials credential = new UsernamePasswordCredentials(); + credential.setUsername(username); + credential.setPassword(new Password(password.toCharArray())); + identityManager.validateCredentials(credential); + if (credential.getStatus() == Credentials.Status.VALID) { + return AuthProviderStatus.SUCCESS; + } else { + return AuthProviderStatus.INVALID_CREDENTIALS; + } + } catch (IdentityManagementException ie) { + throw convertIDMException(ie); } } @@ -70,14 +93,18 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider public boolean updateCredential(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { IdentityManager identityManager = getIdentityManager(realm); - User picketlinkUser = BasicModel.getUser(identityManager, username); - if (picketlinkUser == null) { - logger.debugf("User '%s' doesn't exists. Skip password update", username); - return false; - } + try { + User picketlinkUser = BasicModel.getUser(identityManager, username); + if (picketlinkUser == null) { + logger.debugf("User '%s' doesn't exists. Skip password update", username); + return false; + } - identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray())); - return true; + identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray())); + return true; + } catch (IdentityManagementException ie) { + throw convertIDMException(ie); + } } public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException { @@ -103,4 +130,14 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider } return identityManager; } + + private AuthenticationProviderException convertIDMException(IdentityManagementException ie) { + Throwable realCause = ie; + while (realCause.getCause() != null) { + realCause = realCause.getCause(); + } + + // Use the message from the realCause + return new AuthenticationProviderException(realCause.getMessage(), ie); + } } diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java index 550da90617..898bc1c8e7 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java @@ -30,6 +30,17 @@ public interface AuthenticationProvider { */ AuthUser getUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException; + /** + * Try to register user with this authentication provider + * + * @param realm + * @param configuration + * @param username + * @return ID of newly created user (For example ID from LDAP) + * @throws AuthenticationProviderException if user creation couldn't happen + */ + String registerUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException; + /** * Standard Authentication flow * diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java index 3319cff3bc..823a14b79f 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java @@ -73,7 +73,11 @@ public class AuthenticationProviderManager { public AuthProviderStatus validatePassword(UserModel user, String password) { AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); if (authLink == null) { - authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); + // User not yet linked with any authenticationProvider. Find provider with biggest priority where he is and link + AuthUser authUser = getUser(user.getLoginName()); + authLink = new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId()); + realm.setAuthenticationLink(user, authLink); + logger.infof("User '%s' linked with provider '%s'", authUser.getUsername(), authUser.getProviderName()); } String providerName = authLink.getAuthProvider(); @@ -99,7 +103,38 @@ public class AuthenticationProviderManager { public boolean updatePassword(UserModel user, String password) throws AuthenticationProviderException { AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); if (authLink == null) { - authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); + // Find provider with biggest priority where password update is supported. Then register user here and link him + List configuredProviders = getConfiguredProviderModels(realm); + for (AuthenticationProviderModel providerModel : configuredProviders) { + if (providerModel.isPasswordUpdateSupported()) { + AuthenticationProvider delegate = getProvider(providerModel.getProviderName()); + if (delegate != null) { + AuthUser authUser = delegate.getUser(realm, providerModel.getConfig(), user.getLoginName()); + if (authUser != null) { + // Linking existing user supported just for "model" provider. In other cases throw exception + if (providerModel.getProviderName().equals(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName())) { + authLink = new AuthenticationLinkModel(providerModel.getProviderName(), authUser.getId()); + realm.setAuthenticationLink(user, authLink); + logger.infof("User '%s' linked with provider '%s'", authUser.getUsername(), authUser.getProviderName()); + } else { + throw new AuthenticationProviderException("User " + authUser.getUsername() + " exists in provider " + + authUser.getProviderName() + " but is not linked with model user"); + } + } else { + String userIdInProvider = delegate.registerUser(realm, providerModel.getConfig(), user.getLoginName()); + authLink = new AuthenticationLinkModel(providerModel.getProviderName(), userIdInProvider); + realm.setAuthenticationLink(user, authLink); + logger.infof("User '%s' registered in provider '%s' and linked", user.getLoginName(), providerModel.getProviderName()); + } + break; + } + } + } + + if (authLink == null) { + logger.warnf("No providers found where password update is supported for user '%s'", user.getLoginName()); + return false; + } } String providerName = authLink.getAuthProvider(); @@ -147,7 +182,7 @@ public class AuthenticationProviderManager { return delegate; } - private List getConfiguredProviderModels(RealmModel realm) { + private static List getConfiguredProviderModels(RealmModel realm) { List configuredProviders = realm.getAuthenticationProviders(); // Use model based authentication of current realm by default @@ -159,7 +194,7 @@ public class AuthenticationProviderManager { return configuredProviders; } - private AuthenticationProviderModel getConfiguredProviderModel(RealmModel realm, String providerName) { + public static AuthenticationProviderModel getConfiguredProviderModel(RealmModel realm, String providerName) { List providers = getConfiguredProviderModels(realm); for (AuthenticationProviderModel provider : providers) { if (providerName.equals(provider.getProviderName())) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java index 37cde85958..7bf2e3ae66 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java @@ -23,6 +23,7 @@ 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; @@ -82,6 +83,9 @@ public class AuthProvidersIntegrationTest { @WebResource protected AppPage appPage; + @WebResource + protected RegisterPage registerPage; + @WebResource protected LoginPage loginPage; @@ -111,6 +115,9 @@ public class AuthProvidersIntegrationTest { Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + profilePage.open(); + Assert.assertFalse(profilePage.isPasswordUpdateSupported()); } @Test @@ -120,6 +127,9 @@ public class AuthProvidersIntegrationTest { Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + profilePage.open(); + Assert.assertTrue(profilePage.isPasswordUpdateSupported()); } @Test @@ -131,6 +141,7 @@ public class AuthProvidersIntegrationTest { Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); profilePage.open(); + Assert.assertTrue(profilePage.isPasswordUpdateSupported()); Assert.assertEquals("John", profilePage.getFirstName()); Assert.assertEquals("Doe", profilePage.getLastName()); Assert.assertEquals("john@email.org", profilePage.getEmail()); @@ -191,4 +202,26 @@ public class AuthProvidersIntegrationTest { loginPage.login("john", "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", "email", "registerUserSuccess", "password", "password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java index 582c1120d9..04f1c60dd7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java @@ -96,4 +96,8 @@ public class AccountUpdateProfilePage extends AbstractAccountPage { public String getError() { return errorMessage.getText(); } + + public boolean isPasswordUpdateSupported() { + return driver.getPageSource().contains(PATH + "/password"); + } } diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif index 76295d328a..0debe0bec0 100644 --- a/testsuite/integration/src/test/resources/ldap/users.ldif +++ b/testsuite/integration/src/test/resources/ldap/users.ldif @@ -28,3 +28,13 @@ uid: john cn: John sn: Doe mail: john@email.org + +dn: uid=existing,ou=People,dc=keycloak,dc=org +objectclass: top +objectclass: uidObject +objectclass: person +objectclass: inetOrgPerson +uid: existing +cn: Existing +sn: Foo +mail: existing@email.org