Updating theme templates to render user attributes based on the user profile configuration

Closes #25149

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2023-12-14 21:31:11 -03:00
parent d841971ff4
commit 778847a3ce
22 changed files with 649 additions and 384 deletions

View file

@ -71,3 +71,20 @@ This strategy provides consistency in how attributes are managed by clients and
configuration set to a realm.
For more details, see link:{upgradingguide_link}[{upgradingguide_name}].
= Changes to Freemarker templates to allow rendering pages based on the user profile configuration set to a realm
In this release, the following templates were updated to make it possible to dynamically render attributes based
on the user profile configuration set to a realm:
* `login-update-profile.ftl`
* `register.ftl`
For more details, see link:{upgradingguide_link}[{upgradingguide_name}].
= The update profile page when logging in for the first time through a broker now have its own Freemarker templates
In this release, the server will render the update profile page when the user is authenticating through a broker for the
first time using the `idp-review-user-profile.ftl` template.
For more details, see link:{upgradingguide_link}[{upgradingguide_name}].

View file

@ -70,3 +70,40 @@ this method is not available from the representation payload and it is targeted
= `https-client-auth` is a build time option
Option `https-client-auth` had been treated as a run time option, however this is not supported by Quarkus. The option needs to be handled at build time instead.
= Changes to Freemarker templates to allow rendering pages based on the user profile configuration set to a realm
In this release, the following templates were updated to make it possible to dynamically render attributes based
on the user profile configuration set to a realm:
* `login-update-profile.ftl`
* `register.ftl`
These templates are responsible for rendering both update profile (when the `Update Profile` required action is enabled to a user)
and registration pages, respectively.
If you use a custom theme to change these templates, they will function as expect because only the content is updated.
However, we recommend you to take a look at how to configure a link:{adminguide_link}#user-profile[{declarative user profile}] and possibly avoid
changing the built-in templates by using all the capabilities provided by this feature.
Also, the templates used by the `declarative-user-profile` feature to render the pages for the same flows are longer necessary and removed in this release:
* `update-user-profile.ftl`
* `register-user-profile.ftl`
If you were using the `declarative-user-profile` feature in previously releases with customizations to the above templates,
update the `login-update-profile.ftl` and `register.ftl` accordingly.
= The update profile page when logging in for the first time through a broker now have its own Freemarker templates
In this release, the server will render the update profile page when the user is authenticating through a broker for the
first time using the `idp-review-user-profile.ftl` template.
In previous releases, the template used to update the profile during the first broker login flow was the `login-update-profile.ftl`, the same used
to update profile when users are authenticating to a realm.
By using separate templates for each flow, a more clear distinction exist as to which flow a template is actually used rather than sharing a same template,
and potentially introduce unexpected changes and behavior that should only affect pages for a specific flow.
If you have customizations to the `login-update-profile.ftl` template to customize how users update their profiles when authenticating through a broker, make sure to move your changes
to the new template.

View file

@ -24,9 +24,9 @@ public enum LoginFormsPages {
LOGIN, LOGIN_USERNAME, LOGIN_PASSWORD, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_WEBAUTHN, LOGIN_VERIFY_EMAIL,
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, REGISTER_USER_PROFILE, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE,
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, IDP_REVIEW_USER_PROFILE,
LOGIN_RECOVERY_AUTHN_CODES_INPUT, LOGIN_RECOVERY_AUTHN_CODES_CONFIG,
FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM, UPDATE_EMAIL, LOGIN_RESET_OTP;

View file

@ -337,22 +337,20 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
if (attributes != null) {
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
String key = entry.getKey();
String name = entry.getKey();
if (!isSupportedAttribute(key)) {
if (!isManagedAttribute(key) && isAllowUnmanagedAttribute()) {
unmanagedAttributes.put(key, normalizeAttributeValues(key, entry.getValue()));
if (!isSupportedAttribute(name)) {
if (!isManagedAttribute(name) && isAllowUnmanagedAttribute()) {
String normalizedName = normalizeAttributeName(name);
unmanagedAttributes.put(normalizedName, normalizeAttributeValues(normalizedName, entry.getValue()));
}
continue;
}
if (key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
key = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
}
String normalizedName = normalizeAttributeName(name);
List<String> values = normalizeAttributeValues(normalizedName, entry.getValue());
List<String> values = normalizeAttributeValues(key, entry.getValue());
newAttributes.put(key, Collections.unmodifiableList(values));
newAttributes.put(normalizedName, Collections.unmodifiableList(values));
}
}
@ -398,6 +396,13 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
return newAttributes;
}
private static String normalizeAttributeName(String name) {
if (name.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
return name.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
}
return name;
}
private List<String> normalizeAttributeValues(String name, Object value) {
List<String> values;
@ -472,7 +477,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
}
private boolean isManagedAttribute(String name) {
return metadataByAttribute.containsKey(name);
return metadataByAttribute.containsKey(normalizeAttributeName(name));
}
/**

View file

@ -144,11 +144,31 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
return userCtx.getFirstAttribute(name);
}
@Override
public void setFirstName(String firstName) {
userCtx.setFirstName(firstName);
}
@Override
public void setEmail(String email) {
userCtx.setEmail(email);
}
@Override
public void setLastName(String lastName) {
userCtx.setLastName(lastName);
}
@Override
public String getUsername() {
return userCtx.getUsername();
}
@Override
public void setUsername(String username) {
userCtx.setUsername(username);
}
@Override
public String getServiceAccountClientLink() {
return null;

View file

@ -91,7 +91,6 @@ import org.keycloak.forms.login.MessageType;
import org.keycloak.theme.beans.MessagesPerFieldBean;
import org.keycloak.theme.freemarker.FreeMarkerProvider;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.utils.MediaType;
/**
@ -161,13 +160,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
break;
case UPDATE_PROFILE:
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, new UserUpdateProfileContext(realm, user));
actionMessage = Messages.UPDATE_PROFILE;
if(isDynamicUserProfile()) {
page = LoginFormsPages.UPDATE_USER_PROFILE;
} else {
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
}
break;
case UPDATE_EMAIL:
actionMessage = Messages.UPDATE_EMAIL;
@ -187,9 +181,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case VERIFY_PROFILE:
UpdateProfileContext verifyProfile = new UserUpdateProfileContext(realm, user);
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, verifyProfile);
actionMessage = Messages.UPDATE_PROFILE;
page = LoginFormsPages.UPDATE_USER_PROFILE;
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
break;
default:
return Response.serverError().build();
@ -247,6 +240,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("recoveryAuthnCodesInputBean", new RecoveryAuthnCodeInputLoginBean(session, realm, user));
break;
case LOGIN_UPDATE_PROFILE:
attributes.put("profile", new VerifyProfileBean(user, formData, session));
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
attributes.put("user", new ProfileBean(userCtx, formData));
break;
@ -274,9 +268,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("configuredOtpCredentials", new TotpLoginBean(session, realm, user, (String) this.attributes.get(OTPFormAuthenticator.SELECTED_OTP_CREDENTIAL_ID)));
break;
case REGISTER:
if(isDynamicUserProfile()) {
page = LoginFormsPages.REGISTER_USER_PROFILE;
}
RegisterBean rb = new RegisterBean(formData,session);
//legacy bean for static template
attributes.put("register", rb);
@ -297,11 +288,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case SAML_POST_FORM:
attributes.put("samlPost", new SAMLPostFormBean(formData));
break;
case UPDATE_USER_PROFILE:
attributes.put("profile", new VerifyProfileBean(user, formData, session));
userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
attributes.put("user", new ProfileBean(userCtx, formData));
break;
case IDP_REVIEW_USER_PROFILE:
UpdateProfileContext idpCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
attributes.put("profile", new IdpReviewProfileBean(idpCtx, formData, session));
@ -317,10 +303,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return processTemplate(theme, Templates.getTemplate(page), locale);
}
private boolean isDynamicUserProfile() {
return session.getProvider(UserProfileProvider.class).isEnabled(realm);
}
/**
* Get sure that correct hostname and path is used for totp form.
* Relevant when running in proxy mode.
@ -646,15 +628,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
setMessage(MessageType.WARNING, Messages.UPDATE_PROFILE);
}
if(isDynamicUserProfile()) {
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
if(userCtx != null && userCtx.getUserProfileContext() == UserProfileContext.IDP_REVIEW)
if (userCtx != null && userCtx.getUserProfileContext() == UserProfileContext.IDP_REVIEW) {
return createResponse(LoginFormsPages.IDP_REVIEW_USER_PROFILE);
else
return createResponse(LoginFormsPages.UPDATE_USER_PROFILE);
} else {
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
}
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
}
@Override

View file

@ -62,8 +62,6 @@ public class Templates {
return "select-authenticator.ftl";
case REGISTER:
return "register.ftl";
case REGISTER_USER_PROFILE:
return "register-user-profile.ftl";
case INFO:
return "info.ftl";
case ERROR:
@ -82,8 +80,6 @@ public class Templates {
return "login-x509-info.ftl";
case SAML_POST_FORM:
return "saml-post-form.ftl";
case UPDATE_USER_PROFILE:
return "update-user-profile.ftl";
case IDP_REVIEW_USER_PROFILE:
return "idp-review-user-profile.ftl";
case FRONTCHANNEL_LOGOUT:

View file

@ -83,7 +83,7 @@ public class LegacyAttributes extends DefaultAttributes {
@Override
public Map<String, List<String>> getReadable() {
if(user == null || user.getAttributes() == null) {
return Collections.emptyMap();
return super.getReadable();
}
return new HashMap<>(user.getAttributes());

View file

@ -22,6 +22,7 @@ import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.testsuite.util.UIUtils;
import org.openqa.selenium.By;
@ -66,6 +67,10 @@ public class LoginUpdateProfilePage extends AbstractPage {
prepareUpdate().firstName(firstName).lastName(lastName).email(email).submit();
}
public void update(Map<String, String> attributes) {
prepareUpdate().otherProfileAttribute(attributes).submit();
}
public Update prepareUpdate() {
return new Update(this);
}
@ -176,8 +181,8 @@ public class LoginUpdateProfilePage extends AbstractPage {
return this;
}
public Update otherProfileAttribute(String name, String value) {
other.put(name, value);
public Update otherProfileAttribute(Map<String, String> attributes) {
other.putAll(attributes);
return this;
}

View file

@ -17,8 +17,12 @@
package org.keycloak.testsuite.pages;
import java.util.Map;
import java.util.Map.Entry;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.testsuite.auth.page.AccountFields;
import org.keycloak.testsuite.auth.page.PasswordFields;
@ -76,14 +80,18 @@ public class RegisterPage extends AbstractPage {
private WebElement backToLoginLink;
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
register(firstName, lastName, email, username, password, passwordConfirm, null);
register(firstName, lastName, email, username, password, passwordConfirm, null, null, null);
}
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String department) {
register(firstName, lastName, email, username, password, passwordConfirm, department, null);
register(firstName, lastName, email, username, password, passwordConfirm, department, null, null);
}
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String department, Boolean termsAccepted) {
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, Map<String, String> attributes) {
register(firstName, lastName, email, username, password, passwordConfirm, null, null, attributes);
}
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String department, Boolean termsAccepted, Map<String, String> attributes) {
firstNameInput.clear();
if (firstName != null) {
firstNameInput.sendKeys(firstName);
@ -125,6 +133,12 @@ public class RegisterPage extends AbstractPage {
termsAcceptedInput.click();
}
if (attributes != null) {
for (Entry<String, String> attribute : attributes.entrySet()) {
driver.findElement(By.id(Constants.USER_ATTRIBUTES_PREFIX + attribute.getKey())).sendKeys(attribute.getValue());
}
}
submitButton.click();
}

View file

@ -55,10 +55,10 @@ public class KcOidcBrokerUiLocalesWithIdpHintTest extends AbstractBrokerTest {
driver.getPageSource(), containsString("Jelentkezzen be a fiókjába")); // Sign in to your account
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
waitForPage(driver, "felhasználói fiók adatok módosítása", false); // update account information
waitForPage(driver, "fiók adatainak módosítása", false); // update account information
assertThat("The consumer realm should be in Hungarian even after the redirect from the IDP.",
driver.getPageSource(), containsString("Felhasználói fiók adatok módosítása"));// update account information
driver.getPageSource(), containsString("Fiók adatainak módosítása"));// update account information
assertThat("We must be on correct realm right now",
driver.getCurrentUrl(), containsString("/auth/realms/" + bc.consumerRealmName() + "/"));

View file

@ -727,7 +727,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "registerUserMissingTermsAcceptance@email",
"registerUserMissingTermsAcceptance", "password", "password", null, false);
"registerUserMissingTermsAcceptance", "password", "password", null, false, null);
registerPage.assertCurrent();
assertEquals("You must agree to our terms and conditions.", registerPage.getInputAccountErrors().getTermsError());
@ -753,7 +753,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "registerUserSuccessTermsAcceptance@email",
"registerUserSuccessTermsAcceptance", "password", "password", null, true);
"registerUserSuccessTermsAcceptance", "password", "password", null, true, null);
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());

View file

@ -540,7 +540,7 @@ public class RegisterWithUserProfileTest extends RegisterTest {
registerPage.assertCurrent();
Assert.assertTrue(registerPage.isDepartmentPresent());
registerPage.register("FirstAA", "LastAA", "attributeNotRequiredAndSelectedByScopeCanBeIgnored@email", "attributeNotRequiredAndSelectedByScopeCanBeIgnored", "password", "password", null);
registerPage.register("FirstAA", "LastAA", "attributeNotRequiredAndSelectedByScopeCanBeIgnored@email", "attributeNotRequiredAndSelectedByScopeCanBeIgnored", "password", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));

View file

@ -0,0 +1,102 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.testsuite.theme;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
public class CustomRegistrationTemplateTest extends AbstractTestRealmKeycloakTest {
static final Map<String, String> CUSTOM_ATTRIBUTES = Map.of("street", "street",
"locality",
"locality",
"region", "region",
"postal_code", "postal_code",
"country", "country");
@Page
protected LoginPage loginPage;
@Page
protected RegisterPage registerPage;
@Page
protected AppPage appPage;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setRegistrationAllowed(true);
testRealm.setLoginTheme("address");
}
@Test
public void testRegistration() {
//contains few special characters we want to be sure they are allowed in username
UserRepresentation user = register();
Map<String, List<String>> attributes = user.getAttributes();
assertFalse(attributes.isEmpty());
assertCustomAttributes(attributes);
}
protected static void assertCustomAttributes(Map<String, List<String>> attributes) {
for (Entry<String, String> attribute : CUSTOM_ATTRIBUTES.entrySet()) {
String name = attribute.getKey();
List<String> values = attributes.get(name);
assertNotNull(values);
assertFalse(values.isEmpty());
assertEquals(CUSTOM_ATTRIBUTES.get(name), values.get(0));
}
}
protected UserRepresentation getUser(String username) {
List<UserRepresentation> users = testRealm().users().search(username);
assertFalse(users.isEmpty());
return testRealm().users().get(users.get(0).getId()).toRepresentation();
}
protected UserRepresentation register() {
navigateToRegistrationPage();
String username = "jdoe";
registerPage.register("firstName", "lastName", username + "@keycloak.org", username, "password", "password", CUSTOM_ATTRIBUTES);
UserRepresentation user = getUser(username);
getCleanup().addUserId(user.getId());
return user;
}
private void navigateToRegistrationPage() {
RealmRepresentation realm = testRealm().toRepresentation();
realm.setRegistrationAllowed(true);
testRealm().update(realm);
loginPage.open();
loginPage.clickRegister();
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.testsuite.theme;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile;
import static org.keycloak.testsuite.forms.VerifyProfileTest.enableDynamicUserProfile;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.DECLARATIVE_USER_PROFILE)
public class CustomRegistrationTemplateUserProfileTest extends CustomRegistrationTemplateTest {
private UPConfig upConfig;
@Before
public void onBefore() {
upConfig = updateUserProfileConfiguration();
}
@After
public void onAfter() {
disableDynamicUserProfile(testRealm());
}
@Override
@Test
public void testRegistration() {
upConfig.setUnmanagedAttributePolicy(null);
testRealm().users().userProfile().update(upConfig);
super.testRegistration();
}
@Test
public void testUnmanagedAttributeEnabled() {
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
for (String name : CUSTOM_ATTRIBUTES.keySet()) {
upConfig.removeAttribute(name);
}
testRealm().users().userProfile().update(upConfig);
UserRepresentation user = register();
assertCustomAttributes(user.getAttributes());
}
@Test
public void testUnmanagedAttributeAdminEdit() {
upConfig.setUnmanagedAttributePolicy(null);
for (String name : CUSTOM_ATTRIBUTES.keySet()) {
upConfig.removeAttribute(name);
}
testRealm().users().userProfile().update(upConfig);
UserRepresentation user = register();
assertNull(user.getAttributes());
}
@Test
public void testUnmanagedAttributeDisabled() {
upConfig.setUnmanagedAttributePolicy(null);
for (String name : CUSTOM_ATTRIBUTES.keySet()) {
upConfig.removeAttribute(name);
}
testRealm().users().userProfile().update(upConfig);
UserRepresentation user = register();
assertNull(user.getAttributes());
}
private UPConfig updateUserProfileConfiguration() {
RealmRepresentation realm = testRealm().toRepresentation();
enableDynamicUserProfile(realm);
testRealm().update(realm);
UPConfig upCOnfig = testRealm().users().userProfile().getConfiguration();
upCOnfig.addOrReplaceAttribute(new UPAttribute("street", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("locality", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("region", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("postal_code", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("country", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
testRealm().users().userProfile().update(upCOnfig);
return upCOnfig;
}
}

View file

@ -0,0 +1,127 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.testsuite.theme;
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 java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.util.UserBuilder;
public class CustomUpdateProfileTemplateTest extends AbstractTestRealmKeycloakTest {
static final Map<String, String> CUSTOM_ATTRIBUTES = Map.of("street", "street",
"locality",
"locality",
"region", "region",
"postal_code", "postal_code",
"country", "country");
@Page
protected LoginPage loginPage;
@Page
protected LoginUpdateProfilePage updateProfilePage;
@Page
protected AppPage appPage;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setLoginTheme("address");
// the custom theme expects email as username and the username field is not rendered at all
testRealm.setRegistrationEmailAsUsername(true);
UserRepresentation user = UserBuilder.create().enabled(true)
.username("tom")
.email("tom@keycloak.org")
.password("password")
.firstName("Tom")
.lastName("Brady").build();
testRealm.getUsers().add(user);
}
@Before
public void onBefore() {
UserRepresentation user = getUser("tom");
user.setAttributes(Map.of());
user.setRequiredActions(List.of(UserModel.RequiredAction.UPDATE_PROFILE.name()));
testRealm().users().get(user.getId()).update(user);
}
@Test
public void testUpdateProfile() {
UserRepresentation user = getUser("tom");
Map<String, List<String>> attributes = user.getAttributes();
assertNull(attributes);
user = updateProfile();
assertCustomAttributes(user.getAttributes());
}
protected UserRepresentation updateProfile() {
navigateToUpdateProfilePage();
updateProfilePage.update(CUSTOM_ATTRIBUTES.entrySet().stream()
.map((Function<Entry<String, String>, Entry<String, String>>) entry -> new SimpleEntry<>(Constants.USER_ATTRIBUTES_PREFIX + entry.getKey(), entry.getValue()))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
return getUser("tom");
}
protected static void assertCustomAttributes(Map<String, List<String>> attributes) {
assertNotNull(attributes);
for (Entry<String, String> attribute : CUSTOM_ATTRIBUTES.entrySet()) {
String name = attribute.getKey();
List<String> values = attributes.get(name);
assertNotNull(values);
assertFalse(values.isEmpty());
assertEquals(CUSTOM_ATTRIBUTES.get(name), values.get(0));
}
}
protected UserRepresentation getUser(String username) {
List<UserRepresentation> users = testRealm().users().search(username);
assertFalse(users.isEmpty());
return testRealm().users().get(users.get(0).getId()).toRepresentation();
}
private void navigateToUpdateProfilePage() {
loginPage.open();
loginPage.login("tom@keycloak.org", "password");
updateProfilePage.assertCurrent();
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.testsuite.theme;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile;
import static org.keycloak.testsuite.forms.VerifyProfileTest.enableDynamicUserProfile;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.DECLARATIVE_USER_PROFILE)
public class CustomUpdateProfileTemplateUserProfileTest extends CustomUpdateProfileTemplateTest {
private UPConfig upConfig;
@Before
public void onBefore() {
super.onBefore();
upConfig = updateUserProfileConfiguration();
}
@After
public void onAfter() {
disableDynamicUserProfile(testRealm());
}
@Override
@Test
public void testUpdateProfile() {
upConfig.setUnmanagedAttributePolicy(null);
testRealm().users().userProfile().update(upConfig);
super.testUpdateProfile();
}
@Test
public void testUnmanagedAttributeEnabled() {
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
for (String name : CUSTOM_ATTRIBUTES.keySet()) {
upConfig.removeAttribute(name);
}
testRealm().users().userProfile().update(upConfig);
super.testUpdateProfile();
}
@Test
public void testUnmanagedAttributeAdminEdit() {
upConfig.setUnmanagedAttributePolicy(null);
for (String name : CUSTOM_ATTRIBUTES.keySet()) {
upConfig.removeAttribute(name);
}
testRealm().users().userProfile().update(upConfig);
UserRepresentation user = updateProfile();
assertNull(user.getAttributes());
}
@Test
public void testUnmanagedAttributeDisabled() {
upConfig.setUnmanagedAttributePolicy(null);
for (String name : CUSTOM_ATTRIBUTES.keySet()) {
upConfig.removeAttribute(name);
}
testRealm().users().userProfile().update(upConfig);
UserRepresentation user = updateProfile();
assertNull(user.getAttributes());
}
private UPConfig updateUserProfileConfiguration() {
RealmRepresentation realm = testRealm().toRepresentation();
enableDynamicUserProfile(realm);
testRealm().update(realm);
UPConfig upCOnfig = testRealm().users().userProfile().getConfiguration();
upCOnfig.addOrReplaceAttribute(new UPAttribute("street", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("locality", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("region", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("postal_code", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
upCOnfig.addOrReplaceAttribute(new UPAttribute("country", new UPAttributePermissions(Set.of(ROLE_ADMIN), Set.of(ROLE_USER))));
testRealm().users().userProfile().update(upCOnfig);
return upCOnfig;
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.sssd;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hamcrest.Matchers;
import org.junit.Assert;
@ -205,7 +206,7 @@ public class SSSDUserProfileTest extends AbstractBaseSSSDTest {
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.EMAIL).isEnabled());
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.USERNAME).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById("postal_code").isEnabled());
updateProfilePage.prepareUpdate().otherProfileAttribute("postal_code", "123456").submit();
updateProfilePage.prepareUpdate().otherProfileAttribute(Map.of("postal_code", "123456")).submit();
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();
@ -257,7 +258,7 @@ public class SSSDUserProfileTest extends AbstractBaseSSSDTest {
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.EMAIL).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.USERNAME).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById("postal_code").isEnabled());
updateProfilePage.prepareUpdate().otherProfileAttribute("postal_code", "123456").submit();
updateProfilePage.prepareUpdate().otherProfileAttribute(Map.of("postal_code", "123456")).submit();
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();

View file

@ -1,83 +1,12 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','email','firstName','lastName'); section>
<#import "user-profile-commons.ftl" as userProfileCommons>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
<#if section = "header">
${msg("loginProfileTitle")}
<#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<#if user.editUsernameAllowed>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" value="${(user.username!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
<#if user.editEmailAllowed>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" name="email" value="${(user.email!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('email')>true</#if>"
/>
<#if messagesPerField.existsError('email')>
<span id="input-error-email" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('email'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="firstName" name="firstName" value="${(user.firstName!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('firstName')>true</#if>"
/>
<#if messagesPerField.existsError('firstName')>
<span id="input-error-firstname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('firstName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="lastName" name="lastName" value="${(user.lastName!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('lastName')>true</#if>"
/>
<#if messagesPerField.existsError('lastName')>
<span id="input-error-lastname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('lastName'))?no_esc}
</span>
</#if>
</div>
</div>
<@userProfileCommons.userProfileFormFields/>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
@ -88,7 +17,7 @@
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" formnovalidate/>${msg("doCancel")}</button>
<#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if>

View file

@ -1,94 +0,0 @@
<#import "template.ftl" as layout>
<#import "user-profile-commons.ftl" as userProfileCommons>
<#import "register-commons.ftl" as registerCommons>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
<#if section = "header">
${msg("registerTitle")}
<#elseif section = "form">
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
<@userProfileCommons.userProfileFormFields; callback, attribute>
<#if callback = "afterField">
<#-- render password fields just under the username or email (if used as username) -->
<#if passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> *
</div>
<div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}">
<input type="password" id="password" class="${properties.kcInputClass!}" name="password"
autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm"
class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label> *
</div>
<div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}">
<input type="password" id="password-confirm" class="${properties.kcInputClass!}"
name="password-confirm"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-confirm" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password-confirm')>
<span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
</#if>
</@userProfileCommons.userProfileFormFields>
<@registerCommons.termsAcceptance/>
<#if recaptchaRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}"></div>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doRegister")}"/>
</div>
</div>
</form>
<script type="module" src="${url.resourcesPath}/js/passwordVisibility.js"></script>
</#if>
</@layout.registrationLayout>

View file

@ -1,88 +1,19 @@
<#import "template.ftl" as layout>
<#import "user-profile-commons.ftl" as userProfileCommons>
<#import "register-commons.ftl" as registerCommons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm','termsAccepted'); section>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
<#if section = "header">
${msg("registerTitle")}
<#elseif section = "form">
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
<@userProfileCommons.userProfileFormFields; callback, attribute>
<#if callback = "afterField">
<#-- render password fields just under the username or email (if used as username) -->
<#if passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="firstName" class="${properties.kcInputClass!}" name="firstName"
value="${(register.formData.firstName!'')}"
aria-invalid="<#if messagesPerField.existsError('firstName')>true</#if>"
/>
<#if messagesPerField.existsError('firstName')>
<span id="input-error-firstname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('firstName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="lastName" class="${properties.kcInputClass!}" name="lastName"
value="${(register.formData.lastName!'')}"
aria-invalid="<#if messagesPerField.existsError('lastName')>true</#if>"
/>
<#if messagesPerField.existsError('lastName')>
<span id="input-error-lastname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('lastName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" class="${properties.kcInputClass!}" name="email"
value="${(register.formData.email!'')}" autocomplete="email"
aria-invalid="<#if messagesPerField.existsError('email')>true</#if>"
/>
<#if messagesPerField.existsError('email')>
<span id="input-error-email" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('email'))?no_esc}
</span>
</#if>
</div>
</div>
<#if !realm.registrationEmailAsUsername>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" class="${properties.kcInputClass!}" name="username"
value="${(register.formData.username!'')}" autocomplete="username"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
<#if passwordRequired??>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> *
</div>
<div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}">
@ -98,7 +29,6 @@
</button>
</div>
<#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc}
@ -110,7 +40,7 @@
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm"
class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label> *
</div>
<div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}">
@ -134,6 +64,8 @@
</div>
</div>
</#if>
</#if>
</@userProfileCommons.userProfileFormFields>
<@registerCommons.termsAcceptance/>

View file

@ -1,28 +0,0 @@
<#import "template.ftl" as layout>
<#import "user-profile-commons.ftl" as userProfileCommons>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
<#if section = "header">
${msg("loginProfileTitle")}
<#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<@userProfileCommons.userProfileFormFields/>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" formnovalidate/>${msg("doCancel")}</button>
<#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>