diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml index f1a35c9ba1..a723490bb6 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml @@ -93,6 +93,7 @@ + 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 dce6df6969..023172f9ae 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -24,6 +24,7 @@ public class RealmRepresentation { protected String sslRequired; protected Boolean passwordCredentialGrantAllowed; protected Boolean registrationAllowed; + protected Boolean registrationEmailAsUsername; protected Boolean rememberMe; protected Boolean verifyEmail; protected Boolean resetPasswordAllowed; @@ -264,6 +265,14 @@ public class RealmRepresentation { this.registrationAllowed = registrationAllowed; } + public Boolean isRegistrationEmailAsUsername() { + return registrationEmailAsUsername; + } + + public void setRegistrationEmailAsUsername(Boolean registrationEmailAsUsername) { + this.registrationEmailAsUsername = registrationEmailAsUsername; + } + public Boolean isRememberMe() { return rememberMe; } diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index 7a4404db7f..282b5e4512 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -41,6 +41,7 @@ public interface Errors { String NOT_ALLOWED = "not_allowed"; String FEDERATED_IDENTITY_EMAIL_EXISTS = "federated_identity_email_exists"; + String FEDERATED_IDENTITY_REGISTRATION_EMAIL_MISSING = "federated_identity_registration_email_missing"; String FEDERATED_IDENTITY_USERNAME_EXISTS = "federated_identity_username_exists"; String SSL_REQUIRED = "ssl_required"; diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-login-settings.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-login-settings.html index bd1bab9f84..e491c214fb 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-login-settings.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-login-settings.html @@ -12,7 +12,14 @@
- + + +
+ +
+ +
+
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login.ftl b/forms/common-themes/src/main/resources/theme/login/base/login.ftl index a1a3b23249..e46cf9518d 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login.ftl @@ -17,7 +17,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties index 2f434734f3..0a59a69c69 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties @@ -62,6 +62,7 @@ emailExists=Email already exists federatedIdentityEmailExists=User with email already exists. Please login to account management to link the account. federatedIdentityUsernameExists=User with username already exists. Please login to account management to link the account. +federatedIdentityRegistrationEmailMissing=Email is not provided. Use another provider to create account please. loginTitle=Log in to loginOauthTitle=Temporary access. diff --git a/forms/common-themes/src/main/resources/theme/login/base/register.ftl b/forms/common-themes/src/main/resources/theme/login/base/register.ftl index b8278658a8..492fbabd52 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/register.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/register.ftl @@ -6,6 +6,7 @@ ${rb.registerWith} ${realm.name} <#elseif section = "form"> + <#if !realm.registrationEmailAsUsername>
@@ -14,7 +15,7 @@
- +
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java index f751cf9351..e4ac27f406 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java @@ -48,6 +48,10 @@ public class RealmBean { return realm.isRegistrationAllowed(); } + public boolean isRegistrationEmailAsUsername() { + return realm.isRegistrationEmailAsUsername(); + } + public boolean isResetPasswordAllowed() { return realm.isResetPasswordAllowed(); } 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 4212e3babf..ab260a13a3 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -47,6 +47,10 @@ public interface RealmModel extends RoleContainerModel { void setRegistrationAllowed(boolean registrationAllowed); + public boolean isRegistrationEmailAsUsername(); + + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername); + boolean isPasswordCredentialGrantAllowed(); void setPasswordCredentialGrantAllowed(boolean passwordCredentialGrantAllowed); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 17792e57ef..38baa89319 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -14,6 +14,7 @@ public class RealmEntity extends AbstractIdentifiableEntity { private boolean enabled; private String sslRequired; private boolean registrationAllowed; + protected boolean registrationEmailAsUsername; private boolean rememberMe; private boolean verifyEmail; private boolean passwordCredentialGrantAllowed; @@ -104,6 +105,14 @@ public class RealmEntity extends AbstractIdentifiableEntity { this.registrationAllowed = registrationAllowed; } + public boolean isRegistrationEmailAsUsername() { + return registrationEmailAsUsername; + } + + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + this.registrationEmailAsUsername = registrationEmailAsUsername; + } + public boolean isRememberMe() { return rememberMe; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index d0963dfb4f..686eede868 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -99,6 +99,7 @@ public class ModelToRepresentation { rep.setCertificate(realm.getCertificatePem()); rep.setPasswordCredentialGrantAllowed(realm.isPasswordCredentialGrantAllowed()); rep.setRegistrationAllowed(realm.isRegistrationAllowed()); + rep.setRegistrationEmailAsUsername(realm.isRegistrationEmailAsUsername()); rep.setRememberMe(realm.isRememberMe()); rep.setBruteForceProtected(realm.isBruteForceProtected()); rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds()); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 90b00dd196..ce87adae13 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -85,6 +85,8 @@ public class RepresentationToModel { if (rep.getSslRequired() != null) newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.isPasswordCredentialGrantAllowed() != null) newRealm.setPasswordCredentialGrantAllowed(rep.isPasswordCredentialGrantAllowed()); if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed()); + if (rep.isRegistrationEmailAsUsername() != null) + newRealm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe()); if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); @@ -257,6 +259,7 @@ public class RepresentationToModel { if (rep.getFailureFactor() != null) realm.setFailureFactor(rep.getFailureFactor()); if (rep.isPasswordCredentialGrantAllowed() != null) realm.setPasswordCredentialGrantAllowed(rep.isPasswordCredentialGrantAllowed()); if (rep.isRegistrationAllowed() != null) realm.setRegistrationAllowed(rep.isRegistrationAllowed()); + if (rep.isRegistrationEmailAsUsername() != null) realm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe()); if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index c2cfab83f6..5183b74735 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -152,6 +152,16 @@ public class RealmAdapter implements RealmModel { realm.setRegistrationAllowed(registrationAllowed); } + @Override + public boolean isRegistrationEmailAsUsername() { + return realm.isRegistrationEmailAsUsername(); + } + + @Override + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + realm.setRegistrationEmailAsUsername(registrationEmailAsUsername); + } + @Override public boolean isRememberMe() { return realm.isRememberMe(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index 57013cfbd2..a58774d369 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -108,6 +108,18 @@ public class RealmAdapter implements RealmModel { updated.setRegistrationAllowed(registrationAllowed); } + @Override + public boolean isRegistrationEmailAsUsername() { + if (updated != null) return updated.isRegistrationEmailAsUsername(); + return cached.isRegistrationEmailAsUsername(); + } + + @Override + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + getDelegateForUpdate(); + updated.setRegistrationEmailAsUsername(registrationEmailAsUsername); + } + @Override public boolean isPasswordCredentialGrantAllowed() { if (updated != null) return updated.isPasswordCredentialGrantAllowed(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index 8089e4eb94..dc062401c6 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -33,6 +33,7 @@ public class CachedRealm { private boolean enabled; private SslRequired sslRequired; private boolean registrationAllowed; + private boolean registrationEmailAsUsername; private boolean rememberMe; private boolean verifyEmail; private boolean passwordCredentialGrantAllowed; @@ -92,6 +93,7 @@ public class CachedRealm { enabled = model.isEnabled(); sslRequired = model.getSslRequired(); registrationAllowed = model.isRegistrationAllowed(); + registrationEmailAsUsername = model.isRegistrationEmailAsUsername(); rememberMe = model.isRememberMe(); verifyEmail = model.isVerifyEmail(); passwordCredentialGrantAllowed = model.isPasswordCredentialGrantAllowed(); @@ -205,6 +207,10 @@ public class CachedRealm { return registrationAllowed; } + public boolean isRegistrationEmailAsUsername() { + return registrationEmailAsUsername; + } + public boolean isPasswordCredentialGrantAllowed() { return passwordCredentialGrantAllowed; } 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 9c0e75135f..a17dd1791d 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 @@ -123,6 +123,17 @@ public class RealmAdapter implements RealmModel { em.flush(); } + @Override + public boolean isRegistrationEmailAsUsername() { + return realm.isRegistrationEmailAsUsername(); + } + + @Override + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + realm.setRegistrationEmailAsUsername(registrationEmailAsUsername); + em.flush(); + } + @Override public boolean isRememberMe() { return realm.isRememberMe(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 9a4358b216..1ba7a5573c 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -47,6 +47,8 @@ public class RealmEntity { protected String sslRequired; @Column(name="REGISTRATION_ALLOWED") protected boolean registrationAllowed; + @Column(name = "REGISTRATION_EMAIL_AS_USERNAME") + protected boolean registrationEmailAsUsername; @Column(name="PASSWORD_CRED_GRANT_ALLOWED") protected boolean passwordCredentialGrantAllowed; @Column(name="VERIFY_EMAIL") @@ -183,6 +185,14 @@ public class RealmEntity { this.registrationAllowed = registrationAllowed; } + public boolean isRegistrationEmailAsUsername() { + return registrationEmailAsUsername; + } + + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + this.registrationEmailAsUsername = registrationEmailAsUsername; + } + public boolean isRememberMe() { return rememberMe; } 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 897ac6f836..9bee5a52c0 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 @@ -123,6 +123,15 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + public boolean isRegistrationEmailAsUsername() { + return realm.isRegistrationEmailAsUsername(); + } + + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + realm.setRegistrationEmailAsUsername(registrationEmailAsUsername); + updateRealm(); + } + @Override public boolean isRememberMe() { return realm.isRememberMe(); diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 655b14896e..252a081df4 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -56,6 +56,7 @@ public class ApplianceBootstrap { realm.setAccessCodeLifespanUserAction(300); realm.setSslRequired(SslRequired.EXTERNAL); realm.setRegistrationAllowed(false); + realm.setRegistrationEmailAsUsername(false); KeycloakModelUtils.generateRealmKeys(realm); UserModel adminUser = session.users().addUser(realm, "admin"); diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index d26f695dd8..c95b61807f 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -538,7 +538,20 @@ public class IdentityBrokerService { throw new IdentityBrokerException("federatedIdentityEmailExists"); } - existingUser = this.session.users().getUserByUsername(updatedIdentity.getUsername(), this.realmModel); + String username = updatedIdentity.getUsername(); + if (this.realmModel.isRegistrationEmailAsUsername()) { + username = updatedIdentity.getEmail(); + if (username == null || username.trim().length() == 0) { + fireErrorEvent(Errors.FEDERATED_IDENTITY_REGISTRATION_EMAIL_MISSING); + throw new IdentityBrokerException("federatedIdentityRegistrationEmailMissing"); + // TODO KEYCLOAK-1053 (ask user to enter email address) should be implemented instead of plain exception as better solution for this case + } + username = username.trim(); + } else if (username != null) { + username = username.trim(); + } + + existingUser = this.session.users().getUserByUsername(username, this.realmModel); if (existingUser != null) { fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS); @@ -549,7 +562,7 @@ public class IdentityBrokerService { LOGGER.debugf("Creating account from identity [%s].", federatedIdentityModel); } - UserModel federatedUser = this.session.users().addUser(this.realmModel, updatedIdentity.getUsername()); + UserModel federatedUser = this.session.users().addUser(this.realmModel, username); if (isDebugEnabled()) { LOGGER.debugf("Account [%s] created.", federatedUser); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index f62bd5c551..0198644bd7 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -430,6 +430,10 @@ public class LoginActionsService { String username = formData.getFirst("username"); String email = formData.getFirst("email"); + if (realm.isRegistrationEmailAsUsername()) { + username = email; + formData.putSingle(AuthenticationManager.FORM_USERNAME, username); + } ClientSessionModel clientSession = clientCode.getClientSession(); event.client(clientSession.getClient()) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) @@ -460,7 +464,7 @@ public class LoginActionsService { } // Validate here, so user is not created if password doesn't validate to passwordPolicy of current realm - String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes); + String error = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes); if (error == null) { error = Validation.validatePassword(formData, realm.getPasswordPolicy()); } diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java index a5a0f3f188..254cee1e8c 100755 --- a/services/src/main/java/org/keycloak/services/validation/Validation.java +++ b/services/src/main/java/org/keycloak/services/validation/Validation.java @@ -1,6 +1,7 @@ package org.keycloak.services.validation; import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.messages.Messages; @@ -13,7 +14,7 @@ public class Validation { // Actually allow same emails like angular. See ValidationTest.testEmailValidation() private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*"); - public static String validateRegistrationForm(MultivaluedMap formData, List requiredCredentialTypes) { + public static String validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes) { if (isEmpty(formData.getFirst("firstName"))) { return Messages.MISSING_FIRST_NAME; } @@ -30,7 +31,7 @@ public class Validation { return Messages.INVALID_EMAIL; } - if (isEmpty(formData.getFirst("username"))) { + if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst("username"))) { return Messages.MISSING_USERNAME; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 100f9c1f90..302dd8da86 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -233,6 +233,7 @@ public class AdminAPITest { if (rep.getFailureFactor() != null) Assert.assertEquals(rep.getFailureFactor(), storedRealm.getFailureFactor()); if (rep.isPasswordCredentialGrantAllowed() != null) Assert.assertEquals(rep.isPasswordCredentialGrantAllowed(), storedRealm.isPasswordCredentialGrantAllowed()); if (rep.isRegistrationAllowed() != null) Assert.assertEquals(rep.isRegistrationAllowed(), storedRealm.isRegistrationAllowed()); + if (rep.isRegistrationEmailAsUsername() != null) Assert.assertEquals(rep.isRegistrationEmailAsUsername(), storedRealm.isRegistrationEmailAsUsername()); if (rep.isRememberMe() != null) Assert.assertEquals(rep.isRememberMe(), storedRealm.isRememberMe()); if (rep.isVerifyEmail() != null) Assert.assertEquals(rep.isVerifyEmail(), storedRealm.isVerifyEmail()); if (rep.isResetPasswordAllowed() != null) Assert.assertEquals(rep.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java index 5dd37bc03d..564cb85f45 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -60,10 +60,13 @@ public class RealmTest extends AbstractClientTest { @Test public void updateRealm() { + // first change RealmRepresentation rep = realm.toRepresentation(); rep.setSsoSessionIdleTimeout(123); rep.setSsoSessionMaxLifespan(12); rep.setAccessCodeLifespanLogin(1234); + rep.setRegistrationAllowed(true); + rep.setRegistrationEmailAsUsername(true); realm.update(rep); @@ -72,6 +75,18 @@ public class RealmTest extends AbstractClientTest { assertEquals(123, rep.getSsoSessionIdleTimeout().intValue()); assertEquals(12, rep.getSsoSessionMaxLifespan().intValue()); assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue()); + assertEquals(Boolean.TRUE, rep.isRegistrationAllowed()); + assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername()); + + // second change + rep.setRegistrationAllowed(false); + rep.setRegistrationEmailAsUsername(false); + + realm.update(rep); + + rep = realm.toRepresentation(); + assertEquals(Boolean.FALSE, rep.isRegistrationAllowed()); + assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername()); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index dd034c1e82..b1ae4cbe70 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -59,6 +59,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; + import java.io.IOException; import java.net.URI; import java.util.List; @@ -68,6 +69,7 @@ import static com.thoughtworks.selenium.SeleneseTestBase.fail; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -124,7 +126,7 @@ public abstract class AbstractIdentityProviderTest { public void testSuccessfulAuthentication() { IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - assertSuccessfulAuthentication(identityProviderModel); + assertSuccessfulAuthentication(identityProviderModel, "test-user"); } @Test @@ -132,7 +134,77 @@ public abstract class AbstractIdentityProviderTest { IdentityProviderModel identityProviderModel = getIdentityProviderModel(); identityProviderModel.setUpdateProfileFirstLogin(false); - assertSuccessfulAuthentication(identityProviderModel); + assertSuccessfulAuthentication(identityProviderModel, "test-user"); + } + + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { + + getRealm().setRegistrationEmailAsUsername(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + try { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + identityProviderModel.setUpdateProfileFirstLogin(false); + + authenticateWithIdentityProvider(identityProviderModel, "test-user"); + + // authenticated and redirected to app + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + + // check correct user is created with email as username and bound to correct federated identity + RealmModel realm = getRealm(); + + UserModel federatedUser = session.users().getUserByUsername("test-user@localhost", realm); + + assertNotNull(federatedUser); + + assertEquals("test-user@localhost", federatedUser.getUsername()); + + doAssertFederatedUser(federatedUser, identityProviderModel); + + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); + + assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); + + } finally { + getRealm().setRegistrationEmailAsUsername(false); + } + } + + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername_emailNotProvided() { + + getRealm().setRegistrationEmailAsUsername(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + try { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + identityProviderModel.setUpdateProfileFirstLogin(false); + + authenticateWithIdentityProvider(identityProviderModel, "test-user-noemail"); + + RealmModel realm = getRealm(); + UserModel federatedUser = session.users().getUserByUsername("test-user-noemail", realm); + assertNull(federatedUser); + + // assert page is shown with correct error message + assertEquals("Email is not provided. Use another provider to create account please.", this.driver.findElement(By.className("kc-feedback-text")).getText()); + + } finally { + getRealm().setRegistrationEmailAsUsername(false); + } } @Test @@ -313,7 +385,7 @@ public abstract class AbstractIdentityProviderTest { identityProviderModel.setStoreToken(true); - authenticateWithIdentityProvider(identityProviderModel); + authenticateWithIdentityProvider(identityProviderModel, "test-user"); UserModel federatedUser = getFederatedUser(); RealmModel realm = getRealm(); @@ -435,8 +507,8 @@ public abstract class AbstractIdentityProviderTest { protected abstract void doAssertTokenRetrieval(String pageSource); - private void assertSuccessfulAuthentication(IdentityProviderModel identityProviderModel) { - authenticateWithIdentityProvider(identityProviderModel); + private void assertSuccessfulAuthentication(IdentityProviderModel identityProviderModel, String username) { + authenticateWithIdentityProvider(identityProviderModel, username); // authenticated and redirected to app assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); @@ -464,7 +536,7 @@ public abstract class AbstractIdentityProviderTest { assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); } - private void authenticateWithIdentityProvider(IdentityProviderModel identityProviderModel) { + private void authenticateWithIdentityProvider(IdentityProviderModel identityProviderModel, String username) { driver.navigate().to("http://localhost:8081/test-app"); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/login")); @@ -475,7 +547,7 @@ public abstract class AbstractIdentityProviderTest { assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); // log in to identity provider - this.loginPage.login("test-user", "password"); + this.loginPage.login(username, "password"); doAfterProviderAuthentication(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index 2c2b802b06..2d0657a438 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -193,4 +193,76 @@ public class RegisterTest { events.expectLogin().detail("username", "registerUserSuccess").user(userId).assertEvent(); } + @Test + public void registerExistingUser_emailAsUsername() { + configureRelamRegistrationEmailAsUsername(true); + + try { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.registerWithEmailAsUsername("firstName", "lastName", "test-user@localhost", "password", "password"); + + registerPage.assertCurrent(); + Assert.assertEquals("Username already exists", registerPage.getError()); + + events.expectRegister("test-user@localhost", "test-user@localhost").user((String) null).error("username_in_use").assertEvent(); + } finally { + configureRelamRegistrationEmailAsUsername(false); + } + } + + @Test + public void registerUserMissingOrInvalidEmail_emailAsUsername() { + configureRelamRegistrationEmailAsUsername(true); + + try { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.registerWithEmailAsUsername("firstName", "lastName", null, "password", "password"); + registerPage.assertCurrent(); + Assert.assertEquals("Please specify email", registerPage.getError()); + events.expectRegister(null, null).removeDetail("username").removeDetail("email").error("invalid_registration").assertEvent(); + + registerPage.registerWithEmailAsUsername("firstName", "lastName", "registerUserInvalidEmailemail", "password", "password"); + registerPage.assertCurrent(); + Assert.assertEquals("Invalid email address", registerPage.getError()); + events.expectRegister("registerUserInvalidEmailemail", "registerUserInvalidEmailemail").error("invalid_registration").assertEvent(); + } finally { + configureRelamRegistrationEmailAsUsername(false); + } + } + + @Test + public void registerUserSuccess_emailAsUsername() { + configureRelamRegistrationEmailAsUsername(true); + + try { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.registerWithEmailAsUsername("firstName", "lastName", "registerUserSuccessE@email", "password", "password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + String userId = events.expectRegister("registerUserSuccessE@email", "registerUserSuccessE@email").assertEvent().getUserId(); + events.expectLogin().detail("username", "registerUserSuccessE@email").user(userId).assertEvent(); + } finally { + configureRelamRegistrationEmailAsUsername(false); + } + } + + protected void configureRelamRegistrationEmailAsUsername(final boolean value) { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setRegistrationEmailAsUsername(value); + } + }); + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java index 30807a4f08..b427a1644c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java @@ -17,6 +17,7 @@ public class ModelTest extends AbstractModelTest { public void importExportRealm() { RealmModel realm = realmManager.createRealm("original"); realm.setRegistrationAllowed(true); + realm.setRegistrationEmailAsUsername(true); realm.setResetPasswordAllowed(true); realm.setSslRequired(SslRequired.EXTERNAL); realm.setVerifyEmail(true); @@ -47,6 +48,7 @@ public class ModelTest extends AbstractModelTest { public static void assertEquals(RealmModel expected, RealmModel actual) { Assert.assertEquals(expected.isRegistrationAllowed(), actual.isRegistrationAllowed()); + Assert.assertEquals(expected.isRegistrationEmailAsUsername(), actual.isRegistrationEmailAsUsername()); Assert.assertEquals(expected.isResetPasswordAllowed(), actual.isResetPasswordAllowed()); Assert.assertEquals(expected.getSslRequired(), actual.getSslRequired()); Assert.assertEquals(expected.isVerifyEmail(), actual.isVerifyEmail()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java index d3b95ed01f..217cf19022 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -21,6 +21,9 @@ */ package org.keycloak.testsuite.pages; +import org.junit.Assert; + +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -87,6 +90,42 @@ public class RegisterPage extends AbstractPage { submitButton.click(); } + public void registerWithEmailAsUsername(String firstName, String lastName, String email, String password, String passwordConfirm) { + firstNameInput.clear(); + if (firstName != null) { + firstNameInput.sendKeys(firstName); + } + + lastNameInput.clear(); + if (lastName != null) { + lastNameInput.sendKeys(lastName); + } + + emailInput.clear(); + if (email != null) { + emailInput.sendKeys(email); + } + + try { + usernameInput.clear(); + Assert.fail("Form must be without username field"); + } catch (NoSuchElementException e) { + // OK + } + + passwordInput.clear(); + if (password != null) { + passwordInput.sendKeys(password); + } + + passwordConfirmInput.clear(); + if (passwordConfirm != null) { + passwordConfirmInput.sendKeys(passwordConfirm); + } + + submitButton.click(); + } + public String getError() { return loginErrorMessage != null ? loginErrorMessage.getText() : null; } diff --git a/testsuite/integration/src/test/resources/admin-test/testrealm.json b/testsuite/integration/src/test/resources/admin-test/testrealm.json index 55a2fe1bce..e4adb407bc 100755 --- a/testsuite/integration/src/test/resources/admin-test/testrealm.json +++ b/testsuite/integration/src/test/resources/admin-test/testrealm.json @@ -3,6 +3,7 @@ "enabled": true, "sslRequired": "external", "registrationAllowed": true, + "registrationEmailAsUsername": true, "resetPasswordAllowed": true, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json index dbed98718d..1fd510f7e6 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json +++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-kc-oidc.json @@ -34,6 +34,17 @@ ], "realmRoles": ["manager"] }, + { + "username" : "test-user-noemail", + "enabled": true, + "firstName" : "Test", + "lastName" : "User", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["manager"] + }, { "username" : "pedroigor", "enabled": true, diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json index 3d22c42546..4b3c50518e 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json +++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json @@ -37,6 +37,17 @@ ], "realmRoles": ["manager"] }, + { + "username" : "test-user-noemail", + "enabled": true, + "firstName" : "Test", + "lastName" : "User", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["manager"] + }, { "username" : "pedroigor", "enabled": true, diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json index 19722246ef..60c0396094 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json +++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json @@ -31,6 +31,17 @@ ], "realmRoles": ["manager"] }, + { + "username" : "test-user-noemail", + "enabled": true, + "firstName" : "Test", + "lastName" : "User", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["manager"] + }, { "username" : "pedroigor", "enabled": true,