+
Skip to content

Make UPDATE_EMAIL a supported feature #40273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public enum Feature {

RECOVERY_CODES("Recovery codes", Type.DEFAULT),

UPDATE_EMAIL("Update Email Action", Type.PREVIEW),
UPDATE_EMAIL("Update Email Action", Type.DEFAULT),

FIPS("FIPS 140-2 mode", Type.DISABLED_BY_DEFAULT),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package org.keycloak.federation.kerberos;

import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
Expand Down Expand Up @@ -289,9 +288,7 @@ protected UserModel importUserToKeycloak(RealmModel realm, KerberosPrincipal ker
user.setSingleAttribute(KERBEROS_PRINCIPAL, kerberosPrincipal.toString());

if (kerberosConfig.isUpdateProfileFirstLogin()) {
if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL);
}
user.addRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL);
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
test("sets basic information", async ({ page }) => {
await login(page, user, "pwd", realm);

await page.getByTestId("email").fill(`${user}@somewhere.com`);
await page.getByTestId("firstName").fill("Erik");
await page.getByTestId("lastName").fill("de Wit");
await page.getByTestId("save").click();
Expand Down Expand Up @@ -67,7 +66,7 @@
await login(page, user, "jdoe", realm);

await page.locator("#alternatelang").click();
await page.waitForSelector("text=Italiano");

Check warning on line 69 in js/apps/account-ui/test/personal-info/personal-info.spec.ts

View workflow job for this annotation

GitHub Actions / Account UI

Unexpected use of page.waitForSelector()

await page.locator("#alternatelang").click();
await page.locator("*:focus").press("Control+A");
Expand All @@ -81,7 +80,7 @@
await login(page, user, "jdoe", realm);

await page.locator("#attributes\\.locale").click();
await page.waitForSelector("text=Italiano");

Check warning on line 83 in js/apps/account-ui/test/personal-info/personal-info.spec.ts

View workflow job for this annotation

GitHub Actions / Account UI

Unexpected use of page.waitForSelector()

await page.locator("#attributes\\.locale").click();
await page.locator("*:focus").press("Control+A");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public static void addVerifyProfile(RealmModel realm) {
verifyProfile.setName("Verify Profile");
verifyProfile.setProviderId(UserModel.RequiredAction.VERIFY_PROFILE.name());
verifyProfile.setDefaultAction(false);
verifyProfile.setPriority(90);
verifyProfile.setPriority(100);
realm.addRequiredActionProvider(verifyProfile);
}
}
Expand All @@ -219,7 +219,7 @@ public static void addDeleteCredentialAction(RealmModel realm) {
deleteCredential.setName("Delete Credential");
deleteCredential.setProviderId("delete_credential");
deleteCredential.setDefaultAction(false);
deleteCredential.setPriority(100);
deleteCredential.setPriority(110);
realm.addRequiredActionProvider(deleteCredential);
}
}
Expand All @@ -232,7 +232,7 @@ public static void addIdpLink(RealmModel realm) {
idpLink.setName("Linking Identity Provider");
idpLink.setProviderId("idp_link");
idpLink.setDefaultAction(false);
idpLink.setPriority(110);
idpLink.setPriority(120);
realm.addRequiredActionProvider(idpLink);
}
}
Expand Down Expand Up @@ -287,7 +287,7 @@ public static void addRecoveryAuthnCodesAction(RealmModel realm) {
recoveryCodes.setName("Recovery Authentication Codes");
recoveryCodes.setProviderId(PROVIDER_ID);
recoveryCodes.setDefaultAction(false);
recoveryCodes.setPriority(120);
recoveryCodes.setPriority(130);
realm.addRequiredActionProvider(recoveryCodes);
}
}
Expand All @@ -308,7 +308,7 @@ public static void addWebAuthnRegisterAction(RealmModel realm) {
webauthnRegister.setName("Webauthn Register");
webauthnRegister.setProviderId(PROVIDER_ID);
webauthnRegister.setDefaultAction(false);
webauthnRegister.setPriority(70);
webauthnRegister.setPriority(80);
realm.addRequiredActionProvider(webauthnRegister);
}
}
Expand All @@ -329,7 +329,7 @@ public static void addWebAuthnPasswordlessRegisterAction(RealmModel realm) {
webauthnRegister.setName("Webauthn Register Passwordless");
webauthnRegister.setProviderId(PROVIDER_ID);
webauthnRegister.setDefaultAction(false);
webauthnRegister.setPriority(80);
webauthnRegister.setPriority(90);
realm.addRequiredActionProvider(webauthnRegister);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public enum UserProfileContext {
* In this context, a user profile is managed by themselves when updating their email through an application initiated action.
* In this context, only the {@link UserModel#EMAIL} attribute is supported.
*/
UPDATE_EMAIL(false, true, false, Set.of(UserModel.EMAIL)::contains);
UPDATE_EMAIL(false, true, true, Set.of(UserModel.EMAIL)::contains);

private final boolean resetEmailVerified;
private final Predicate<String> attributeSelector;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ private static boolean editEmailCondition(AttributeContext c) {
}

if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
if (UPDATE_PROFILE.equals(c.getContext())) {
if (!isNewUser(c)) {
if (c.getUser().getEmail() == null || c.getUser().getEmail().isEmpty()) {
// allow to set email via UPDATE_PROFILE if the email is not set for the user
return true;
}
} else if (UserModel.EMAIL.equals(c.getAttribute().getKey()) && c.getAttribute().getValue().isEmpty()) {
return true;
}
}
return !(UPDATE_PROFILE.equals(c.getContext()) || ACCOUNT.equals(c.getContext()));
}

Expand All @@ -170,6 +180,16 @@ private static boolean readEmailCondition(AttributeContext c) {
}

if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
if (UPDATE_PROFILE.equals(c.getContext())) {
if (!isNewUser(c)) {
if (c.getUser().getEmail() == null || c.getUser().getEmail().isEmpty()) {
// show email field in UPDATE_PROFILE page if the email is not set for the user
return true;
}
} else if (UserModel.EMAIL.equals(c.getAttribute().getKey()) && c.getAttribute().getValue().isEmpty()) {
return true;
}
}
return !UPDATE_PROFILE.equals(context);
}

Expand Down Expand Up @@ -233,9 +253,7 @@ public void init(Config.Scope config) {
addContextualProfileMetadata(configureUserProfile(createBrokeringProfile(readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createAccountProfile(ACCOUNT, readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_PROFILE, readOnlyValidator)));
if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_EMAIL, readOnlyValidator)));
}
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_EMAIL, readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createRegistrationUserCreationProfile(readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ public class CustomConfigTest {
Keycloak adminClient;

@Test
public void testUpdateEmailFeatureEnabled() {
Optional<FeatureRepresentation> updateEmailFeature = adminClient.serverInfo().getInfo().getFeatures().stream().filter(f -> f.getName().equals(Profile.Feature.UPDATE_EMAIL.name())).findFirst();
Assertions.assertTrue(updateEmailFeature.isPresent());
Assertions.assertTrue(updateEmailFeature.get().isEnabled());
public void testPasskeyFeatureEnabled() {
Optional<FeatureRepresentation> passKeysFeature = adminClient.serverInfo().getInfo().getFeatures().stream().filter(f -> f.getName().equals(Profile.Feature.PASSKEYS.name())).findFirst();
Assertions.assertTrue(passKeysFeature.isPresent());
Assertions.assertTrue(passKeysFeature.get().isEnabled());
}

public static class CustomServerConfig implements KeycloakServerConfig {

@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.UPDATE_EMAIL);
return config.features(Profile.Feature.PASSKEYS);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public void testRequiredActions() {
addRequiredAction(expected, "CONFIGURE_RECOVERY_AUTHN_CODES", "Recovery Authentication Codes", true, false, null);
addRequiredAction(expected, "CONFIGURE_TOTP", "Configure OTP", true, false, null);
addRequiredAction(expected, "TERMS_AND_CONDITIONS", "Terms and Conditions", false, false, null);
addRequiredAction(expected, "UPDATE_EMAIL", "Update Email", true, false, null);
addRequiredAction(expected, "UPDATE_PASSWORD", "Update Password", true, false, null);
addRequiredAction(expected, "UPDATE_PROFILE", "Update Profile", true, false, null);
addRequiredAction(expected, "VERIFY_EMAIL", "Verify Email", true, false, null);
Expand Down
Loading
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载