diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationMappingRepresentation.java deleted file mode 100644 index ecbfb8d6bb..0000000000 --- a/core/src/main/java/org/keycloak/representations/idm/AuthenticationMappingRepresentation.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.keycloak.representations.idm; - -import java.util.List; - -/** - * @author Marek Posolda - */ -public class AuthenticationMappingRepresentation { - - protected String self; // link - protected String username; - protected List authenticationLinks; - - public String getSelf() { - return self; - } - - public void setSelf(String self) { - this.self = self; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public List getAuthenticationLinks() { - return authenticationLinks; - } - - public void setAuthenticationLinks(List authenticationLinks) { - this.authenticationLinks = authenticationLinks; - } - - -} diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index fc72ef02c7..a4278d254f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -39,7 +39,6 @@ public class RealmRepresentation { protected Map> applicationRoleMappings; protected Map> applicationScopeMappings; protected List socialMappings; - protected List authenticationMappings; protected List applications; protected List oauthClients; protected Map socialProviders; @@ -181,18 +180,6 @@ public class RealmRepresentation { return mapping; } - public List getAuthenticationMappings() { - return authenticationMappings; - } - - public AuthenticationMappingRepresentation authenticationMapping(String username) { - AuthenticationMappingRepresentation mapping = new AuthenticationMappingRepresentation(); - mapping.setUsername(username); - if (authenticationMappings == null) authenticationMappings = new ArrayList(); - authenticationMappings.add(mapping); - return mapping; - } - public Set getRequiredCredentials() { return requiredCredentials; } diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 5fc55f0b7c..43aa368f7a 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -20,6 +20,7 @@ public class UserRepresentation { protected String firstName; protected String lastName; protected String email; + protected AuthenticationLinkRepresentation authenticationLink; protected Map attributes; protected List credentials; protected List requiredActions; @@ -96,6 +97,14 @@ public class UserRepresentation { this.emailVerified = emailVerified; } + public AuthenticationLinkRepresentation getAuthenticationLink() { + return authenticationLink; + } + + public void setAuthenticationLink(AuthenticationLinkRepresentation authenticationLink) { + this.authenticationLink = authenticationLink; + } + public Map getAttributes() { return attributes; } diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 827f628048..1fd0843adf 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -134,11 +134,9 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa boolean removeSocialLink(UserModel user, String socialProvider); - UserModel getUserByAuthenticationLink(AuthenticationLinkModel authenticationLink); + AuthenticationLinkModel getAuthenticationLink(UserModel user); - Set getAuthenticationLinks(UserModel user); - - void addAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink); + void setAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink); boolean isSocial(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 08e6f3989e..c0483afdab 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -395,7 +395,9 @@ public class RealmAdapter implements RealmModel { private void removeUser(UserEntity user) { em.createQuery("delete from " + UserRoleMappingEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate(); em.createQuery("delete from " + SocialLinkEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate(); - em.createQuery("delete from " + AuthenticationLinkEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate(); + if (user.getAuthenticationLink() != null) { + em.remove(user.getAuthenticationLink()); + } em.remove(user); } @@ -614,43 +616,22 @@ public class RealmAdapter implements RealmModel { } @Override - public UserModel getUserByAuthenticationLink(AuthenticationLinkModel authenticationLink) { - TypedQuery query = em.createNamedQuery("findUserByAuthLinkAndRealm", UserEntity.class); - query.setParameter("realm", realm); - query.setParameter("authProvider", authenticationLink.getAuthProvider()); - query.setParameter("authUserId", authenticationLink.getAuthUserId()); - List results = query.getResultList(); - if (results.isEmpty()) { - return null; - } else if (results.size() > 1) { - throw new IllegalStateException("More results found for authenticationProvider=" + authenticationLink.getAuthProvider() + - ", authUserId=" + authenticationLink.getAuthUserId() + ", results=" + results); - } else { - UserEntity user = results.get(0); - return new UserAdapter(user); - } + public AuthenticationLinkModel getAuthenticationLink(UserModel user) { + UserEntity userEntity = ((UserAdapter) user).getUser(); + AuthenticationLinkEntity authLinkEntity = userEntity.getAuthenticationLink(); + return authLinkEntity == null ? null : new AuthenticationLinkModel(authLinkEntity.getAuthProvider(), authLinkEntity.getAuthUserId()); } @Override - public Set getAuthenticationLinks(UserModel user) { - TypedQuery query = em.createNamedQuery("findAuthLinkByUser", AuthenticationLinkEntity.class); - query.setParameter("user", ((UserAdapter) user).getUser()); - List results = query.getResultList(); - Set set = new HashSet(); - for (AuthenticationLinkEntity entity : results) { - set.add(new AuthenticationLinkModel(entity.getAuthProvider(), entity.getAuthUserId())); - } - return set; - } - - @Override - public void addAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { + public void setAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { AuthenticationLinkEntity entity = new AuthenticationLinkEntity(); - entity.setRealm(realm); entity.setAuthProvider(authenticationLink.getAuthProvider()); entity.setAuthUserId(authenticationLink.getAuthUserId()); - entity.setUser(((UserAdapter) user).getUser()); + + UserEntity userEntity = ((UserAdapter) user).getUser(); + userEntity.setAuthenticationLink(entity); em.persist(entity); + em.persist(userEntity); em.flush(); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java index 0eecfba5be..e6ae8d421c 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java @@ -6,16 +6,13 @@ import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; import org.hibernate.annotations.GenericGenerator; /** * @author Marek Posolda */ -@NamedQueries({ - @NamedQuery(name="findAuthLinkByUser", query="select link from AuthenticationLinkEntity link where link.user = :user"), - @NamedQuery(name="findUserByAuthLinkAndRealm", query="select link.user from AuthenticationLinkEntity link where link.realm = :realm and link.authProvider = :authProvider and link.authUserId = :authUserId") -}) @Entity public class AuthenticationLinkEntity { @@ -24,12 +21,6 @@ public class AuthenticationLinkEntity { @GeneratedValue(generator = "keycloak_generator") private String id; - @ManyToOne - private UserEntity user; - - @ManyToOne - protected RealmEntity realm; - protected String authProvider; protected String authUserId; @@ -41,22 +32,6 @@ public class AuthenticationLinkEntity { this.id = id; } - public UserEntity getUser() { - return user; - } - - public void setUser(UserEntity user) { - this.user = user; - } - - public RealmEntity getRealm() { - return realm; - } - - public void setRealm(RealmEntity realm) { - this.realm = realm; - } - public String getAuthProvider() { return authProvider; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index ab25a647db..9d8435d61d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -15,6 +15,8 @@ import javax.persistence.MapKeyColumn; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; +import javax.persistence.OneToOne; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -64,6 +66,9 @@ public class UserEntity { @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) protected Collection credentials = new ArrayList(); + @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true) + protected AuthenticationLinkEntity authenticationLink; + public String getId() { return id; } @@ -160,6 +165,14 @@ public class UserEntity { this.credentials = credentials; } + public AuthenticationLinkEntity getAuthenticationLink() { + return authenticationLink; + } + + public void setAuthenticationLink(AuthenticationLinkEntity authenticationLink) { + this.authenticationLink = authenticationLink; + } + public int getNotBefore() { return notBefore; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index fb9619a70f..e5f66966b4 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -927,41 +927,26 @@ public class RealmAdapter extends AbstractMongoAdapter implements R } @Override - public UserModel getUserByAuthenticationLink(AuthenticationLinkModel authenticationLink) { - DBObject query = new QueryBuilder() - .and("authenticationLinks.authProvider").is(authenticationLink.getAuthProvider()) - .and("authenticationLinks.authUserId").is(authenticationLink.getAuthUserId()) - .and("realmId").is(getId()) - .get(); - UserEntity userEntity = getMongoStore().loadSingleEntity(UserEntity.class, query, invocationContext); - return userEntity==null ? null : new UserAdapter(userEntity, invocationContext); - } - - @Override - public Set getAuthenticationLinks(UserModel user) { + public AuthenticationLinkModel getAuthenticationLink(UserModel user) { UserEntity userEntity = ((UserAdapter)user).getUser(); - List linkEntities = userEntity.getAuthenticationLinks(); + AuthenticationLinkEntity authLinkEntity = userEntity.getAuthenticationLink(); - if (linkEntities == null) { - return Collections.EMPTY_SET; + if (authLinkEntity == null) { + return null; + } else { + return new AuthenticationLinkModel(authLinkEntity.getAuthProvider(), authLinkEntity.getAuthUserId()); } - - Set result = new HashSet(); - for (AuthenticationLinkEntity authLinkEntity : linkEntities) { - AuthenticationLinkModel model = new AuthenticationLinkModel(authLinkEntity.getAuthProvider(), authLinkEntity.getAuthUserId()); - result.add(model); - } - return result; } @Override - public void addAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { + public void setAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { UserEntity userEntity = ((UserAdapter)user).getUser(); AuthenticationLinkEntity authLinkEntity = new AuthenticationLinkEntity(); authLinkEntity.setAuthProvider(authenticationLink.getAuthProvider()); authLinkEntity.setAuthUserId(authenticationLink.getAuthUserId()); + userEntity.setAuthenticationLink(authLinkEntity); - getMongoStore().pushItemToList(userEntity, "authenticationLinks", authLinkEntity, true, invocationContext); + getMongoStore().updateEntity(userEntity, invocationContext); } protected void updateRealm() { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java index f0539df323..96b87919eb 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java @@ -33,7 +33,7 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo private List requiredActions; private List credentials = new ArrayList(); private List socialLinks; - private List authenticationLinks; + private AuthenticationLinkEntity authenticationLink; @MongoField public String getLoginName() { @@ -163,11 +163,11 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo } @MongoField - public List getAuthenticationLinks() { - return authenticationLinks; + public AuthenticationLinkEntity getAuthenticationLink() { + return authenticationLink; } - public void setAuthenticationLinks(List authenticationLinks) { - this.authenticationLinks = authenticationLinks; + public void setAuthenticationLink(AuthenticationLinkEntity authenticationLink) { + this.authenticationLink = authenticationLink; } } diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java index 3f57de3434..e365d1e134 100644 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java @@ -95,10 +95,10 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { Assert.assertEquals("john@email.org", john2.getEmail()); // Verify link exists - Set authLinks = realm2.getAuthenticationLinks(john2); - Assert.assertEquals(1, authLinks.size()); - AuthenticationLinkModel authLink = authLinks.iterator().next(); + AuthenticationLinkModel authLink = realm2.getAuthenticationLink(john2); + Assert.assertNotNull(authLink); Assert.assertEquals(authLink.getAuthProvider(), AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL); + Assert.assertEquals(authLink.getAuthUserId(), realm1.getUser("john").getId()); } finally { ResteasyProviderFactory.clearContextData(); } @@ -110,14 +110,19 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { // Add externalModel authenticationProvider into realm2 and point to realm1 setupAuthenticationProviders(); + // Add john to realm2 and set authentication link + UserModel john = realm2.addUser("john"); + john.setEnabled(true); + realm2.setAuthenticationLink(john, new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL, realm1.getUser("john").getId())); + try { // this is needed for externalModel provider ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession); - // Change credential via realm2 and validate that they are changed in both realms + // Change credential via realm2 and validate that they are changed also in realm1 AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm2); try { - authProviderManager.updatePassword("john", "password-updated"); + Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); @@ -132,14 +137,14 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { // Change credential and validate that password is updated just for realm2 try { - authProviderManager.updatePassword("john", "password-updated2"); + Assert.assertFalse(authProviderManager.updatePassword(john, "password-updated2")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); } formData = createFormData("john", "password-updated2"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm1, formData)); - Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm2, formData)); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm2, formData)); // Allow passwordUpdate propagation again @@ -148,7 +153,7 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { // Set passwordPolicy for realm1 and verify that password update fail realm1.setPasswordPolicy(new PasswordPolicy("length(8)")); try { - authProviderManager.updatePassword("john", "passw"); + authProviderManager.updatePassword(john, "passw"); Assert.fail("Update not expected to pass"); } catch (AuthenticationProviderException ape) { diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java index 96157fe762..1711d22d49 100644 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java @@ -95,9 +95,8 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { Assert.assertEquals("john@email.org", john.getEmail()); // Verify link exists - Set authLinks = realm.getAuthenticationLinks(john); - Assert.assertEquals(1, authLinks.size()); - AuthenticationLinkModel authLink = authLinks.iterator().next(); + AuthenticationLinkModel authLink = realm.getAuthenticationLink(john); + Assert.assertNotNull(authLink); Assert.assertEquals(authLink.getAuthProvider(), AuthProviderConstants.PROVIDER_NAME_PICKETLINK); } finally { ResteasyProviderFactory.clearContextData(); @@ -114,6 +113,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { // Add some user and password to realm UserModel realmUser = realm.addUser("realmUser"); + realmUser.setEnabled(true); UserCredentialModel credential = new UserCredentialModel(); credential.setType(CredentialRepresentation.PASSWORD); credential.setValue("pass"); @@ -135,6 +135,11 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { realmUser.setEnabled(false); formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.ACCOUNT_DISABLED, am.authenticateForm(realm, formData)); + + // Successful authentication + realmUser.setEnabled(true); + formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass"); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData)); } finally { ResteasyProviderFactory.clearContextData(); } @@ -149,26 +154,34 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { // this is needed for ldap provider ResteasyProviderFactory.pushContext(KeycloakRegistry.class, new KeycloakRegistry()); + LdapTestUtils.setLdapPassword(realm, "john", "password"); + + // First authenticate successfully to sync john into realm + MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password"); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData)); + // Change credential and validate that user can authenticate AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm); + + UserModel john = realm.getUser("john"); try { - authProviderManager.updatePassword("john", "password-updated"); + Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); } - MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated"); + formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData)); // Password updated just in LDAP, so validating directly in realm should fail - Assert.assertFalse(realm.validatePassword(realm.getUser("john"), "password-updated")); + Assert.assertFalse(realm.validatePassword(john, "password-updated")); // Switch to not allow updating passwords in ldap AuthProvidersExternalModelTest.setPasswordUpdateForProvider(false, AuthProviderConstants.PROVIDER_NAME_PICKETLINK, realm); // Change credential and validate that password is not updated try { - authProviderManager.updatePassword("john", "password-updated2"); + Assert.assertFalse(authProviderManager.updatePassword(john, "password-updated2")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); diff --git a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java index ce4ca5363d..fb8ebaabf9 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java @@ -208,24 +208,9 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(authProv3.isPasswordUpdateSupported()); // Test authentication linking - Set authLinks = realm.getAuthenticationLinks(socialUser); - Assert.assertEquals(2, authLinks.size()); - boolean plFound = false; - boolean extFound = false; - for (AuthenticationLinkModel authLinkModel : authLinks) { - if (AuthProviderConstants.PROVIDER_NAME_PICKETLINK.equals(authLinkModel.getAuthProvider())) { - plFound = true; - Assert.assertEquals(authLinkModel.getAuthUserId(), "myUser1"); - } else if (AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL.equals(authLinkModel.getAuthProvider())) { - extFound = true; - Assert.assertEquals(authLinkModel.getAuthUserId(), "myUser11"); - } - } - Assert.assertTrue(plFound && extFound); - - UserModel foundAuthUser = realm.getUserByAuthenticationLink(new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, "myUser1")); - Assert.assertEquals(foundAuthUser.getLoginName(), socialUser.getLoginName()); - Assert.assertNull(realm.getUserByAuthenticationLink(new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, "not-existing"))); + AuthenticationLinkModel authLink = realm.getAuthenticationLink(socialUser); + Assert.assertEquals(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, authLink.getAuthProvider()); + Assert.assertEquals("myUser1", authLink.getAuthUserId()); commit(); diff --git a/model/tests/src/test/resources/testrealm.json b/model/tests/src/test/resources/testrealm.json index b28087d7c8..56bf23ee71 100755 --- a/model/tests/src/test/resources/testrealm.json +++ b/model/tests/src/test/resources/testrealm.json @@ -75,7 +75,11 @@ }, { "username": "mySocialUser", - "enabled": true + "enabled": true, + "authenticationLink": { + "authProvider": "picketlink", + "authUserId": "myUser1" + } } ], "socialMappings": [ @@ -100,21 +104,6 @@ ] } ], - "authenticationMappings": [ - { - "username": "mySocialUser", - "authenticationLinks": [ - { - "authProvider": "picketlink", - "authUserId": "myUser1" - }, - { - "authProvider": "externalModel", - "authUserId": "myUser11" - } - ] - } - ], "applications": [ { "name": "Application", 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 fe86e5b603..ff6c61207c 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -16,8 +16,7 @@ import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.resources.RealmsResource; import org.keycloak.spi.authentication.AuthProviderStatus; -import org.keycloak.spi.authentication.AuthResult; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProviderManager; import org.keycloak.util.Time; @@ -188,6 +187,25 @@ public class AuthenticationManager { } UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); + if (user == null) { + AuthUser authUser = AuthenticationProviderManager.getManager(realm).getUser(username); + if (authUser != null) { + // Create new user and link him with authentication provider + user = realm.addUser(authUser.getUsername()); + user.setEnabled(true); + user.setFirstName(authUser.getFirstName()); + user.setLastName(authUser.getLastName()); + user.setEmail(authUser.getEmail()); + realm.setAuthenticationLink(user, new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId())); + } else { + logger.warn("User " + username + " not found"); + return AuthenticationStatus.INVALID_USER; + } + } + + if (!checkEnabled(user)) { + return AuthenticationStatus.ACCOUNT_DISABLED; + } Set types = new HashSet(); @@ -202,20 +220,13 @@ public class AuthenticationManager { return AuthenticationStatus.MISSING_PASSWORD; } - if (user == null && types.contains(CredentialRepresentation.TOTP)) { - logger.warn("User doesn't exists and TOTP is required for the realm"); - return AuthenticationStatus.INVALID_USER; - } - - if (user != null && user.isTotp()) { + if (user.isTotp()) { String token = formData.getFirst(CredentialRepresentation.TOTP); if (token == null) { logger.warn("TOTP token not provided"); return AuthenticationStatus.MISSING_TOTP; } - if (!checkEnabled(user)) { - return AuthenticationStatus.ACCOUNT_DISABLED; - } + logger.debug("validating TOTP"); if (!realm.validateTOTP(user, password, token)) { return AuthenticationStatus.INVALID_CREDENTIALS; @@ -223,58 +234,12 @@ public class AuthenticationManager { } else { logger.debug("validating password for user: " + username); - AuthResult authResult = AuthenticationProviderManager.getManager(realm).validatePassword(username, password); - if (authResult.getAuthProviderStatus() == AuthProviderStatus.INVALID_CREDENTIALS) { + AuthProviderStatus authStatus = AuthenticationProviderManager.getManager(realm).validatePassword(user, password); + if (authStatus == AuthProviderStatus.INVALID_CREDENTIALS) { logger.debug("invalid password for user: " + username); return AuthenticationStatus.INVALID_CREDENTIALS; - } else if (authResult.getAuthProviderStatus() == AuthProviderStatus.USER_NOT_FOUND) { - logger.debug("User " + username + " not found in any Authentication provider"); - return AuthenticationStatus.INVALID_USER; - } - - if (authResult.getAuthenticatedUser() != null) { - AuthenticatedUser authUser = authResult.getAuthenticatedUser(); - AuthenticationLinkModel authLink = new AuthenticationLinkModel(authResult.getProviderName(), authUser.getId()); - user = realm.getUserByAuthenticationLink(authLink); - if (user == null) { - user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); - if (user != null) { - // Case when we already have user with the same username like authenticated, but he is not yet linked to current provider. - // TODO: Revisit if it's ok to link if we allow to change username. Maybe ask user? - // TODO: Update of existing account? - realm.addAuthenticationLink(user, authLink); - logger.info("User " + authUser.getUsername() + " successfully authenticated and linked with provider " + authResult.getProviderName()); - } else { - // Create new user, which has been successfully authenticated and link him with authentication provider - user = realm.addUser(authUser.getUsername()); - user.setEnabled(true); - user.setFirstName(authUser.getFirstName()); - user.setLastName(authUser.getLastName()); - user.setEmail(authUser.getEmail()); - - realm.addAuthenticationLink(user, authLink); - logger.info("User " + username + " successfully authenticated and created based on provider " + authResult.getProviderName()); - } - } else { - // Existing and linked user has been authenticated TODO: Update of existing account? - } - - // Authenticated username could be different from the "form" username. In this case, we will change it - if (!username.equals(user.getLoginName())) { - formData.putSingle(FORM_USERNAME, user.getLoginName()); - logger.debug("Existing user " + user.getLoginName() + " successfully authenticated"); - } - - } else { - // Authentication provider didn't send AuthenticatedUser. Using already retrieved user based on username from "form" - if (user == null) { - logger.warn("User '" + username + "' successfully authenticated, but he doesn't exists and don't know how to create him"); - return AuthenticationStatus.INVALID_USER; - } - } - - if (!checkEnabled(user)) { - return AuthenticationStatus.ACCOUNT_DISABLED; + } else if (authStatus == AuthProviderStatus.FAILED) { + return AuthenticationStatus.FAILED; } } diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 5b4c735ae6..d9d40d2926 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -21,7 +21,6 @@ import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.AuthenticationLinkRepresentation; -import org.keycloak.representations.idm.AuthenticationMappingRepresentation; import org.keycloak.representations.idm.AuthenticationProviderRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.OAuthClientRepresentation; @@ -388,15 +387,6 @@ public class RealmManager { } } } - if (rep.getAuthenticationMappings() != null) { - for (AuthenticationMappingRepresentation authMapping : rep.getAuthenticationMappings()) { - UserModel user = userMap.get(authMapping.getUsername()); - for (AuthenticationLinkRepresentation link : authMapping.getAuthenticationLinks()) { - AuthenticationLinkModel mappingModel = new AuthenticationLinkModel(link.getAuthProvider(), link.getAuthUserId()); - newRealm.addAuthenticationLink(user, mappingModel); - } - } - } if (rep.getSmtpServer() != null) { newRealm.setSmtpConfig(new HashMap(rep.getSmtpServer())); @@ -478,6 +468,11 @@ public class RealmManager { newRealm.updateCredential(user, credential); } } + if (userRep.getAuthenticationLink() != null) { + AuthenticationLinkRepresentation link = userRep.getAuthenticationLink(); + AuthenticationLinkModel authLink = new AuthenticationLinkModel(link.getAuthProvider(), link.getAuthUserId()); + newRealm.setAuthenticationLink(user, authLink); + } return 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 4ac9a08cfd..b937674b58 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -264,13 +264,15 @@ public class AccountService { AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm); if (Validation.isEmpty(password)) { return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); - // TODO: This may not work in some cases. For example if ldap username is "foo" but actual loginName of user is "bar", which could theoretically happen... - } else if (authProviderManager.validatePassword(user.getLoginName(), password).getAuthProviderStatus() != AuthProviderStatus.SUCCESS) { + } else if (authProviderManager.validatePassword(user, password) != AuthProviderStatus.SUCCESS) { return account.setError(Messages.INVALID_PASSWORD_EXISTING).createResponse(AccountPages.PASSWORD); } try { - authProviderManager.updatePassword(user.getLoginName(), passwordNew); + boolean passwordUpdateSuccess = authProviderManager.updatePassword(user, passwordNew); + if (!passwordUpdateSuccess) { + return account.setError("Password update failed").createResponse(AccountPages.PASSWORD); + } } catch (AuthenticationProviderException ape) { return account.setError(ape.getMessage()).createResponse(AccountPages.PASSWORD); } diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index c6b0a6ca85..d77ba7e6ee 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -200,7 +200,10 @@ public class RequiredActionsService { } try { - AuthenticationProviderManager.getManager(realm).updatePassword(user.getLoginName(), passwordNew); + boolean updateSuccessful = AuthenticationProviderManager.getManager(realm).updatePassword(user, passwordNew); + if (!updateSuccessful) { + return loginForms.setError("Password update failed").createResponse(RequiredAction.UPDATE_PASSWORD); + } } catch (AuthenticationProviderException ape) { return loginForms.setError(ape.getMessage()).createResponse(RequiredAction.UPDATE_PASSWORD); } 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 00fbbb400d..7a91471153 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -397,12 +397,21 @@ public class TokenService { UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.PASSWORD); credentials.setValue(formData.getFirst("password")); + + boolean passwordUpdateSuccessful; + String passwordUpdateError = null; try { - AuthenticationProviderManager.getManager(realm).updatePassword(username, formData.getFirst("password")); + passwordUpdateSuccessful = AuthenticationProviderManager.getManager(realm).updatePassword(user, formData.getFirst("password")); + passwordUpdateError = "Password update failed"; } catch (AuthenticationProviderException ape) { - // User already registered, but force him to update password + passwordUpdateSuccessful = false; + passwordUpdateError = ape.getMessage(); + } + + // User already registered, but force him to update password + if (!passwordUpdateSuccessful) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - return Flows.forms(realm, request, uriInfo).setError(ape.getMessage()).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); + return Flows.forms(realm, request, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } } 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 58bf42390f..66166ab0e0 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 @@ -9,8 +9,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.spi.authentication.AuthProviderStatus; -import org.keycloak.spi.authentication.AuthResult; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProvider; import org.keycloak.spi.authentication.AuthenticationProviderException; @@ -24,22 +23,19 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat private static final Logger logger = Logger.getLogger(AbstractModelAuthenticationProvider.class); @Override - public AuthResult validatePassword(RealmModel currentRealm, Map config, String username, String password) throws AuthenticationProviderException { + public AuthUser getUser(RealmModel currentRealm, Map config, String username) throws AuthenticationProviderException { RealmModel realm = getRealm(currentRealm, config); + UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); + return user == null ? null : createAuthenticatedUserInstance(user); + } + @Override + public AuthProviderStatus validatePassword(RealmModel currentRealm, Map config, String username, String password) throws AuthenticationProviderException { + RealmModel realm = getRealm(currentRealm, config); UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); - if (user == null) { - return new AuthResult(AuthProviderStatus.USER_NOT_FOUND); - } - boolean result = realm.validatePassword(user, password); - if (!result) { - return new AuthResult(AuthProviderStatus.INVALID_CREDENTIALS); - } - - AuthenticatedUser authUser = createAuthenticatedUserInstance(user); - return new AuthResult(AuthProviderStatus.SUCCESS).setProviderName(getName()).setUser(authUser); + return result ? AuthProviderStatus.SUCCESS : AuthProviderStatus.INVALID_CREDENTIALS; } @Override @@ -54,7 +50,7 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat UserModel user = realm.getUser(username); if (user == null) { - logger.debugf("User '%s' doesn't exists. Skip password update", username); + logger.warnf("User '%s' doesn't exists. Skip password update", username); return false; } @@ -68,5 +64,9 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat protected abstract RealmModel getRealm(RealmModel currentRealm, Map config) throws AuthenticationProviderException; - protected abstract AuthenticatedUser createAuthenticatedUserInstance(UserModel user); + protected AuthUser createAuthenticatedUserInstance(UserModel user) { + return new AuthUser(user.getId(), user.getLoginName(), getName()) + .setName(user.getFirstName(), user.getLastName()) + .setEmail(user.getEmail()); + } } diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java index a24a4952d7..a4d129bdc3 100644 --- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java +++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java @@ -7,7 +7,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.spi.authentication.AuthProviderConstants; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProviderException; /** @@ -40,11 +40,4 @@ public class ExternalModelAuthenticationProvider extends AbstractModelAuthentica } return realm; } - - @Override - protected AuthenticatedUser createAuthenticatedUserInstance(UserModel user) { - return new AuthenticatedUser(user.getId(), user.getLoginName()) - .setName(user.getFirstName(), user.getLastName()) - .setEmail(user.getEmail()); - } } diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java index 516948920a..29ab43aafe 100644 --- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java +++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java @@ -5,7 +5,7 @@ import java.util.Map; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.spi.authentication.AuthProviderConstants; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; /** * AbstractModelAuthenticationProvider, which uses current realm to call operations on @@ -23,10 +23,4 @@ public class ModelAuthenticationProvider extends AbstractModelAuthenticationProv protected RealmModel getRealm(RealmModel currentRealm, Map config) { return currentRealm; } - - @Override - protected AuthenticatedUser createAuthenticatedUserInstance(UserModel user) { - // We don't want AuthenticatedUser instance. Auto-registration won't never happen with this provider - return null; - } } 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 9d87e769d7..ffc1ab6b37 100644 --- 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 @@ -6,9 +6,8 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.models.RealmModel; import org.keycloak.spi.authentication.AuthProviderStatus; -import org.keycloak.spi.authentication.AuthResult; import org.keycloak.spi.authentication.AuthProviderConstants; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProvider; import org.keycloak.spi.authentication.AuthenticationProviderException; import org.keycloak.spi.picketlink.PartitionManagerProvider; @@ -36,28 +35,27 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider } @Override - public AuthResult validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { + public AuthUser getUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException { IdentityManager identityManager = getIdentityManager(realm); - User picketlinkUser = BasicModel.getUser(identityManager, username); - if (picketlinkUser == null) { - return new AuthResult(AuthProviderStatus.USER_NOT_FOUND); - } + return picketlinkUser == null ? null : new AuthUser(picketlinkUser.getId(), picketlinkUser.getLoginName(), getName()) + .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) + .setEmail(picketlinkUser.getEmail()) + .setProviderName(getName()); + } + + @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) { - AuthResult result = new AuthResult(AuthProviderStatus.SUCCESS); - - AuthenticatedUser authenticatedUser = new AuthenticatedUser(picketlinkUser.getId(), picketlinkUser.getLoginName()) - .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) - .setEmail(picketlinkUser.getEmail()); - result.setUser(authenticatedUser).setProviderName(getName()); - return result; + return AuthProviderStatus.SUCCESS; } else { - return new AuthResult(AuthProviderStatus.INVALID_CREDENTIALS); + return AuthProviderStatus.INVALID_CREDENTIALS; } } diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java index 56d05d0543..c782dfc707 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java @@ -7,6 +7,6 @@ package org.keycloak.spi.authentication; */ public enum AuthProviderStatus { - SUCCESS, INVALID_CREDENTIALS, USER_NOT_FOUND + SUCCESS, INVALID_CREDENTIALS, FAILED } diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java deleted file mode 100644 index fd52d679aa..0000000000 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.keycloak.spi.authentication; - -/** - * @author Marek Posolda - */ -public class AuthResult { - - // Status of authentication - private final AuthProviderStatus authProviderStatus; - - // Provider, which authenticated user - private String providerName; - - // filled usually only in case of successful authentication and just with some Authentication providers - private AuthenticatedUser authenticatedUser; - - public AuthResult(AuthProviderStatus authProviderStatus) { - this.authProviderStatus = authProviderStatus; - } - - public AuthResult setProviderName(String providerName) { - this.providerName = providerName; - return this; - } - - public AuthResult setUser(AuthenticatedUser user) { - this.authenticatedUser = user; - return this; - } - - public AuthProviderStatus getAuthProviderStatus() { - return authProviderStatus; - } - - public String getProviderName() { - return providerName; - } - - public AuthenticatedUser getAuthenticatedUser() { - return authenticatedUser; - } - - -} diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticatedUser.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthUser.java similarity index 64% rename from spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticatedUser.java rename to spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthUser.java index 760f99ce2f..e0e6f76ca6 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticatedUser.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthUser.java @@ -3,7 +3,7 @@ package org.keycloak.spi.authentication; /** * @author Marek Posolda */ -public class AuthenticatedUser { +public class AuthUser { private String id; private String username; @@ -11,16 +11,19 @@ public class AuthenticatedUser { private String lastName; private String email; - public AuthenticatedUser(String id, String username) { + private String providerName; + + public AuthUser(String id, String username, String providerName) { this.id = id; this.username = username; + this.providerName = providerName; } public String getId() { return id; } - public AuthenticatedUser setId(String id) { + public AuthUser setId(String id) { this.id = id; return this; } @@ -29,7 +32,7 @@ public class AuthenticatedUser { return username; } - public AuthenticatedUser setUsername(String username) { + public AuthUser setUsername(String username) { this.username = username; return this; } @@ -38,7 +41,7 @@ public class AuthenticatedUser { return firstName; } - public AuthenticatedUser setName(String name) { + public AuthUser setName(String name) { int i = name.lastIndexOf(' '); if (i != -1) { firstName = name.substring(0, i); @@ -50,7 +53,7 @@ public class AuthenticatedUser { return this; } - public AuthenticatedUser setName(String firstName, String lastName) { + public AuthUser setName(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; return this; @@ -64,8 +67,17 @@ public class AuthenticatedUser { return email; } - public AuthenticatedUser setEmail(String email) { + public AuthUser setEmail(String email) { this.email = email; return this; } + + public String getProviderName() { + return providerName; + } + + public AuthUser setProviderName(String providerName) { + this.providerName = providerName; + return this; + } } 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 1ab1836ef0..014d432ba7 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 @@ -11,6 +11,17 @@ public interface AuthenticationProvider { String getName(); + /** + * Get user by given username or email. Return user instance or null if user doesn't exists in this authentication provider + * + * @param realm + * @param configuration + * @param username or email + * @return found user or null if user with given username doesn't exists + * @throws AuthenticationProviderException + */ + AuthUser getUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException; + /** * Standard Authentication flow * @@ -18,7 +29,7 @@ public interface AuthenticationProvider { * @param password * @return result of authentication, which might eventually encapsulate info about authenticated user and provider which successfully authenticated */ - AuthResult validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException; + AuthProviderStatus validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException; /** 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 8c6a6b8ad0..c6a59a1a8f 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 @@ -1,14 +1,15 @@ package org.keycloak.spi.authentication; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jboss.logging.Logger; +import org.keycloak.models.AuthenticationLinkModel; import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.util.ProviderLoader; /** @@ -46,77 +47,107 @@ public class AuthenticationProviderManager { this.delegates = delegates; } - public AuthResult validatePassword(String username, String password) { - List configuredProviders = getConfiguredProviders(realm); - boolean userExists = false; - - for (AuthenticationProviderModel authProviderConfig : configuredProviders) { - String providerName = authProviderConfig.getProviderName(); - - AuthenticationProvider delegate = getDelegate(providerName); + public AuthUser getUser(String username) { + List authProviderModels = getConfiguredProviderModels(realm); + for (AuthenticationProviderModel providerModel : authProviderModels) { + AuthenticationProvider delegate = getProvider(providerModel.getProviderName()); if (delegate == null) { continue; } try { - AuthResult currentResult = delegate.validatePassword(realm, authProviderConfig.getConfig(), username, password); - logger.debugf("Authentication provider '%s' finished with '%s' for authentication of '%s'", delegate.getName(), currentResult.getAuthProviderStatus().toString(), username); - - if (currentResult.getAuthProviderStatus() == AuthProviderStatus.SUCCESS) { - return currentResult; - } else if (currentResult.getAuthProviderStatus() == AuthProviderStatus.INVALID_CREDENTIALS) { - userExists = true; + AuthUser authUser = delegate.getUser(realm, providerModel.getConfig(), username); + if (authUser != null) { + logger.debugf("User '%s' found with provider '%s'", username, providerModel.getProviderName()); + return authUser; } } catch (AuthenticationProviderException ape) { logger.warn(ape.getMessage(), ape); } } - AuthProviderStatus status = userExists ? AuthProviderStatus.INVALID_CREDENTIALS : AuthProviderStatus.USER_NOT_FOUND; - logger.debugf("Not able to authenticate '%s' with any authentication provider. Status: '%s'", username, status.toString()); - - return new AuthResult(status); + logger.debugf("User '%s' not found with any provider", username); + return null; } - public void updatePassword(String username, String password) throws AuthenticationProviderException { - List configuredProviders = getConfiguredProviders(realm); + public AuthProviderStatus validatePassword(UserModel user, String password) { + AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); + if (authLink == null) { + authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); + } - for (AuthenticationProviderModel authProviderConfig : configuredProviders) { + String providerName = authLink.getAuthProvider(); - // Update just those, which support password update - if (authProviderConfig.isPasswordUpdateSupported()) { - String providerName = authProviderConfig.getProviderName(); - AuthenticationProvider delegate = getDelegate(providerName); - if (delegate == null) { - continue; - } + AuthenticationProviderModel providerModel = getConfiguredProviderModel(realm, providerName); + AuthenticationProvider delegate = getProvider(providerName); + if (delegate == null || providerModel == null) { + return AuthProviderStatus.FAILED; + } - try { - if (delegate.updateCredential(realm, authProviderConfig.getConfig(), username, password)) { - logger.debugf("Updated password in authentication provider '%s' for user '%s'", delegate.getName(), username); - } else { - logger.debugf("Password not updated in authentication provider '%s' for user '%s'", delegate.getName(), username); - } - } catch (AuthenticationProviderException ape) { - // Rethrow it to upper layer - logger.warn("Failed to update password: " + ape.getMessage()); - throw ape; - } - } else { - logger.debugf("Skip password update for authentication provider '%s' for user '%s'", authProviderConfig.getProviderName(), username); - } + try { + checkCorrectAuthLink(delegate, providerModel, authLink, user.getLoginName()); + + AuthProviderStatus currentResult = delegate.validatePassword(realm, providerModel.getConfig(), user.getLoginName(), password); + logger.debugf("Authentication provider '%s' finished with '%s' for authentication of '%s'", delegate.getName(), currentResult.toString(), user.getLoginName()); + return currentResult; + } catch (AuthenticationProviderException ape) { + logger.warn(ape.getMessage(), ape); + return AuthProviderStatus.FAILED; } } - private AuthenticationProvider getDelegate(String providerName) { + 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()); + } + + String providerName = authLink.getAuthProvider(); + + AuthenticationProviderModel providerModel = getConfiguredProviderModel(realm, providerName); + if (providerModel == null) { + return false; + } + + String username = user.getLoginName(); + + // Update just those, which support password update + if (providerModel.isPasswordUpdateSupported()) { + try { + AuthenticationProvider delegate = getProvider(providerName); + if (delegate == null) { + return false; + } + + checkCorrectAuthLink(delegate, providerModel, authLink, username); + + if (delegate.updateCredential(realm,providerModel.getConfig(), user.getLoginName(), password)) { + logger.debugf("Updated password in authentication provider '%s' for user '%s'", providerName, username); + return true; + } else { + logger.warnf("Password not updated in authentication provider '%s' for user '%s'", providerName, username); + return false; + } + } catch (AuthenticationProviderException ape) { + // Rethrow it to upper layer + logger.warn("Failed to update password: " + ape.getMessage()); + throw ape; + } + } else { + logger.warnf("Skip password update for authentication provider '%s' for user '%s'", providerName, username); + return false; + } + } + + private AuthenticationProvider getProvider(String providerName) { AuthenticationProvider delegate = delegates.get(providerName); if (delegate == null) { - logger.warnf("Configured provider with name '%s' not found", providerName); + logger.warnf("Provider '%s' not available on classpath", providerName); } return delegate; } - private List getConfiguredProviders(RealmModel realm) { + private List getConfiguredProviderModels(RealmModel realm) { List configuredProviders = realm.getAuthenticationProviders(); // Use model based authentication of current realm by default @@ -127,4 +158,27 @@ public class AuthenticationProviderManager { return configuredProviders; } + + private AuthenticationProviderModel getConfiguredProviderModel(RealmModel realm, String providerName) { + List providers = getConfiguredProviderModels(realm); + for (AuthenticationProviderModel provider : providers) { + if (providerName.equals(provider.getProviderName())) { + return provider; + } + } + + logger.warnf("Provider '%s' not configured in realm", providerName); + return null; + } + + // Check if ID of linked AuthUser is same as expected ID from authenticationLink . It should catch the case when for example user "john" was deleted in LDAP + // and then user "john" has been created again, but it's actually different user with different ID + private void checkCorrectAuthLink(AuthenticationProvider authProvider, AuthenticationProviderModel providerModel, + AuthenticationLinkModel authLinkModel, String username) throws AuthenticationProviderException { + AuthUser authUser = authProvider.getUser(realm, providerModel.getConfig(), username); + String userExternalId = authUser.getId(); + if (!userExternalId.equals(authLinkModel.getAuthUserId())) { + throw new AuthenticationProviderException("ID did not match! ID from provider: " + userExternalId + ", ID from authentication link: " + authLinkModel.getAuthUserId()); + } + } } 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 c34e77e9cd..37cde85958 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 @@ -47,7 +47,7 @@ public class AuthProvidersIntegrationTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { addUser(appRealm, "mary", "mary@test.com", "password-app"); - addUser(adminstrationRealm, "mary", "mary@admin.com", "password-admin"); + addUser(adminstrationRealm, "mary-admin", "mary@admin.com", "password-admin"); AuthenticationProviderModel modelProvider = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_MODEL, false, Collections.EMPTY_MAP); AuthenticationProviderModel picketlinkProvider = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, true, Collections.EMPTY_MAP); @@ -116,7 +116,7 @@ public class AuthProvidersIntegrationTest { @Test public void loginExternalModel() { loginPage.open(); - loginPage.login("mary", "password-admin"); + loginPage.login("mary-admin", "password-admin"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -148,18 +148,18 @@ public class AuthProvidersIntegrationTest { try { changePasswordPage.open(); - loginPage.login("mary", "password-admin"); + loginPage.login("mary-admin", "password-admin"); // Can't update to "pass" due to passwordPolicy changePasswordPage.changePassword("password-admin", "pass", "pass"); Assert.assertEquals("Invalid password: minimum length 6", profilePage.getError()); - changePasswordPage.changePassword("password-app", "password-updated", "password-updated"); + changePasswordPage.changePassword("password-admin", "password-updated", "password-updated"); Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); changePasswordPage.logout(); loginPage.open(); - loginPage.login("mary", "password-updated"); + loginPage.login("mary-admin", "password-updated"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); } finally {