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" : [