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

@ -70,4 +70,21 @@ are marshalled and unmarshalled when using both Admin and Account REST APIs.
This strategy provides consistency in how attributes are managed by clients and makes sure they conform to the user profile
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

@ -69,4 +69,41 @@ 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.
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;
}
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)
return createResponse(LoginFormsPages.IDP_REVIEW_USER_PROFILE);
else
return createResponse(LoginFormsPages.UPDATE_USER_PROFILE);
} else {
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
if (userCtx != null && userCtx.getUserProfileContext() == UserProfileContext.IDP_REVIEW) {
return createResponse(LoginFormsPages.IDP_REVIEW_USER_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!}">
@ -87,13 +16,13 @@
<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>
<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")}" />
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>
</@layout.registrationLayout>

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,139 +1,71 @@
<#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">
<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>
<@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>
<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>
</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>
<#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')>
<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>
<#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 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>
</#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>