diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl index 22575087ff..93ec4fe1d6 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl @@ -6,6 +6,15 @@ ${msg("loginProfileTitle")} <#elseif section = "form">
+
style="display:none"> +
+ +
+
+ +
+
+
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 b161ad21fd..e6ae21de98 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 @@ -66,6 +66,10 @@ public class RealmBean { return realm.isInternationalizationEnabled(); } + public boolean isEditUsernameAllowed() { + return realm.isEditUsernameAllowed(); + } + public boolean isPassword() { for (RequiredCredentialModel r : realm.getRequiredCredentials()) { if (r.getType().equals(CredentialRepresentation.PASSWORD)) { diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java index 42c2e02348..ac7862b7ad 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java @@ -52,7 +52,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact RealmModel realm = context.getRealm(); - List errors = Validation.validateUpdateProfileForm(formData); + List errors = Validation.validateUpdateProfileForm(realm, formData); if (errors != null && !errors.isEmpty()) { Response challenge = context.form() .setErrors(errors) @@ -62,6 +62,10 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact return; } + if (realm.isEditUsernameAllowed()) { + user.setUsername(formData.getFirst("username")); + } + user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java index 77b6d19f6f..f16dcee7db 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -123,6 +123,10 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { return expectLogin().event(event).removeDetail(Details.CONSENT).session(isUUID()); } + public ExpectedEvent expectRequiredActionEnabledUsername(EventType event, String newUsername) { + return expectLogin(newUsername).event(event).removeDetail(Details.CONSENT).session(isUUID()); + } + public ExpectedEvent expectLogin() { return expect(EventType.LOGIN) .detail(Details.CODE_ID, isCodeId()) @@ -134,6 +138,17 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { .session(isUUID()); } + public ExpectedEvent expectLogin(String username) { + return expect(EventType.LOGIN, username) + .detail(Details.CODE_ID, isCodeId()) + //.detail(Details.USERNAME, DEFAULT_USERNAME) + //.detail(Details.AUTH_METHOD, OIDCLoginProtocol.LOGIN_PROTOCOL) + //.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE) + .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI) + .detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED) + .session(isUUID()); + } + public ExpectedEvent expectClientLogin() { return expect(EventType.CLIENT_LOGIN) .detail(Details.CODE_ID, isCodeId()) @@ -202,6 +217,16 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { .event(event); } + public ExpectedEvent expect(EventType event, String username) { + return new ExpectedEvent() + .realm(DEFAULT_REALM) + .client(DEFAULT_CLIENT_ID) + .user(keycloak.getUser(DEFAULT_REALM, username).getId()) + .ipAddress(DEFAULT_IP_ADDRESS) + .session((String) null) + .event(event); + } + @Override public EventListenerProvider create(KeycloakSession session) { return new EventListenerProvider() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java index 868a76cc50..87a5640aeb 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java @@ -121,7 +121,7 @@ public class RequiredActionMultipleActionsTest { } public String updateProfile(String sessionId) { - updateProfilePage.update("New first", "New last", "new@email.com"); + updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com"); if (sessionId != null) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index e3d6ac921d..57ce53c1cd 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -21,11 +21,7 @@ */ package org.keycloak.testsuite.actions; -import org.junit.Assert; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.RealmModel; @@ -75,6 +71,8 @@ public class RequiredActionUpdateProfileTest { public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + UserModel anotherUser = manager.getSession().users().getUserByEmail("john-doh@localhost", appRealm); + anotherUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); } }); } @@ -87,7 +85,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com"); + updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId(); events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent(); @@ -101,6 +99,31 @@ public class RequiredActionUpdateProfileTest { Assert.assertEquals("New first", user.getFirstName()); Assert.assertEquals("New last", user.getLastName()); Assert.assertEquals("new@email.com", user.getEmail()); + Assert.assertEquals("test-user@localhost", user.getUsername()); + } + + @Test + public void updateUsername() { + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", "john-doh@localhost", "new"); + + String sessionId = events.expectRequiredActionEnabledUsername(EventType.UPDATE_PROFILE, "new").assertEvent().getSessionId(); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin("new").session(sessionId).assertEvent(); + + // assert user is really updated in persistent store + UserRepresentation user = keycloakRule.getUser("test", "new"); + Assert.assertEquals("New first", user.getFirstName()); + Assert.assertEquals("New last", user.getLastName()); + Assert.assertEquals("john-doh@localhost", user.getEmail()); + Assert.assertEquals("new", user.getUsername()); } @Test @@ -111,7 +134,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("", "New last", "new@email.com"); + updateProfilePage.update("", "New last", "new@email.com", "new"); updateProfilePage.assertCurrent(); @@ -133,7 +156,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "", "new@email.com"); + updateProfilePage.update("New first", "", "new@email.com", "new"); updateProfilePage.assertCurrent(); @@ -155,7 +178,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", ""); + updateProfilePage.update("New first", "New last", "", "new"); updateProfilePage.assertCurrent(); @@ -177,7 +200,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "invalidemail"); + updateProfilePage.update("New first", "New last", "invalidemail", "invalid"); updateProfilePage.assertCurrent(); @@ -191,6 +214,29 @@ public class RequiredActionUpdateProfileTest { events.assertEmpty(); } + @Test + public void updateProfileMissingUsername() { + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", "new@email.com", ""); + + updateProfilePage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("New first", updateProfilePage.getFirstName()); + Assert.assertEquals("New last", updateProfilePage.getLastName()); + Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); + Assert.assertEquals("", updateProfilePage.getUsername()); + + Assert.assertEquals("Please specify username.", updateProfilePage.getError()); + + events.assertEmpty(); + } + @Test public void updateProfileDuplicatedEmail() { loginPage.open(); @@ -199,7 +245,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "keycloak-user@localhost"); + updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); updateProfilePage.assertCurrent(); 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 fb2b5dfd4b..9037e2b4a0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -17,7 +17,6 @@ */ package org.keycloak.testsuite.broker; -import org.codehaus.jackson.map.ObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -452,7 +451,7 @@ public abstract class AbstractIdentityProviderTest { doAfterProviderAuthentication(); this.updateProfilePage.assertCurrent(); - this.updateProfilePage.update("Test", "User", "psilva@redhat.com"); + this.updateProfilePage.update("Test", "User", "psilva@redhat.com", "psilva"); WebElement element = this.driver.findElement(By.className("kc-feedback-text")); @@ -461,7 +460,7 @@ public abstract class AbstractIdentityProviderTest { assertEquals("Email already exists.", element.getText()); this.updateProfilePage.assertCurrent(); - this.updateProfilePage.update("Test", "User", "test-user@redhat.com"); + this.updateProfilePage.update("Test", "User", "test-user@redhat.com", "test-user"); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); @@ -725,7 +724,7 @@ public abstract class AbstractIdentityProviderTest { // update profile this.updateProfilePage.assertCurrent(); - this.updateProfilePage.update(userFirstName, userLastName, userEmail); + this.updateProfilePage.update(userFirstName, userLastName, userEmail, username); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java index d67862c792..f1bbe5f477 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -38,19 +38,24 @@ public class LoginUpdateProfilePage extends AbstractPage { @FindBy(id = "email") private WebElement emailInput; + @FindBy(id = "username") + private WebElement usernameInput; + @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; @FindBy(className = "feedback-error") private WebElement loginErrorMessage; - public void update(String firstName, String lastName, String email) { + public void update(String firstName, String lastName, String email, String username) { firstNameInput.clear(); firstNameInput.sendKeys(firstName); lastNameInput.clear(); lastNameInput.sendKeys(lastName); emailInput.clear(); emailInput.sendKeys(email); + usernameInput.clear(); + usernameInput.sendKeys(username); submitButton.click(); } @@ -70,6 +75,10 @@ public class LoginUpdateProfilePage extends AbstractPage { return emailInput.getAttribute("value"); } + public String getUsername() { + return usernameInput.getAttribute("value"); + } + public boolean isCurrent() { return driver.getTitle().equals("Update Account Information"); } diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index 8ad29ccfcf..5344ad0e74 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -5,6 +5,7 @@ "sslRequired": "external", "registrationAllowed": true, "resetPasswordAllowed": true, + "editUsernameAllowed" : 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", "requiredCredentials": [ "password" ], @@ -32,7 +33,23 @@ } }, { - "username" : "keycloak-user@localhost", + "username" : "john-doh@localhost", + "enabled": true, + "email" : "john-doh@localhost", + "firstName": "John", + "lastName": "Doh", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "clientRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + }, + { + "username" : "keycloak-user@localhost", "enabled": true, "email" : "keycloak-user@localhost", "credentials" : [