8000 Reset phoneNumberVerified when users change their phone number by ryan-morris · Pull Request #53020 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ image:images/client-scopes-phone.png[]

When the *phone* client scope is linked to a client, the client automatically inherits all the protocol mappers defined in the *phone* client scope. Access tokens issued for this client contain the phone number information about the user, assuming that the user has a defined phone number.

The `phone_number` and `phone_number_verified` claims are mapped from the `phoneNumber` and `phoneNumberVerified` user attributes. {project_name} does not verify phone numbers itself. However, when users change their own `phoneNumber` in the Account Console, through the update profile required action, or when reviewing their profile during their first login through an identity provider, a `phoneNumberVerified` value of `true` is set to `false`, in the same way the email verification status is reset when users change their email. Values written by identity provider mappers or user federation providers are stored as provided. If you add the `phoneNumberVerified` attribute to the user profile, make sure that only administrators can edit it.

Built-in client scopes contain the protocol mappers as defined in the specification. You are free to edit client scopes and create, update, or remove any protocol mappers or role scope mappings.

== Consent related settings
Expand Down
8000
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
*/
public final class DefaultUserProfile implements UserProfile {

// the attributes mapped to the phone_number and phone_number_verified claims by the built-in "phone" client scope
private static final String PHONE_NUMBER = "phoneNumber";
private static final String PHONE_NUMBER_VERIFIED = "phoneNumberVerified";

private final UserProfileMetadata metadata;
private final Function<Attributes, UserModel> userSupplier;
private final Attributes attributes;
Expand Down Expand Up @@ -128,6 +132,12 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri
try {
Map<String, List<String>> writable = new HashMap<>(attributes.getWritable());

boolean phoneNumberVerifiedReset = false;
// the number is removed below, which resets the flag, so a submitted flag must not be written first
boolean phoneNumberRemoved = removeAttributes && metadata.getContext().isResetPhoneNumberVerified()
&& user.getAttributes().containsKey(PHONE_NUMBER) && !attributes.nameSet().contains(PHONE_NUMBER)
&& !attributes.isReadOnly(PHONE_NUMBER);

for (Map.Entry<String, List<String>> attribute : writable.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
String name = attribute.getKey();
List<String> currentValue = user.getAttributeStream(name)
Expand All @@ -138,6 +148,10 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri
continue;
}

if (PHONE_NUMBER_VERIFIED.equals(name) && (phoneNumberVerifiedReset || phoneNumberRemoved)) {
continue;
}

if (isIgnoreAttributeUpdate(removeAttributes, updatedValue, name)) {
continue;
}
Expand All @@ -152,6 +166,11 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri
user.setEmailVerified(false);
}

if (PHONE_NUMBER.equals(name) && metadata.getContext().isResetPhoneNumberVerified()) {
resetPhoneNumberVerified(user, changeListener);
phoneNumberVerifiedReset = true;
}

for (AttributeChangeListener listener : changeListener) {
listener.onChange(name, user, currentValue);
}
Expand All @@ -165,6 +184,11 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri

attrsToRemove.removeAll(attributes.nameSet());

if (phoneNumberVerifiedReset || phoneNumberRemoved) {
// reset rather than removed
attrsToRemove.remove(PHONE_NUMBER_VERIFIED);
}

for (String name : attrsToRemove) {
if (attributes.isReadOnly(name)) {
continue;
Expand All @@ -184,6 +208,10 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri
user.removeAttribute(name);
}

if (PHONE_NUMBER.equals(name) && metadata.getContext().isResetPhoneNumberVerified()) {
resetPhoneNumberVerified(user, changeListener);
}

for (AttributeChangeListener listener : changeListener) {
listener.onChange(name, user, currentValue);
}
Expand Down Expand Up @@ -216,6 +244,20 @@ private boolean isIgnoreAttributeUpdate(boolean removeAttributes, List<String> u
return false;
}

private void resetPhoneNumberVerified(UserModel user, AttributeChangeListener... changeListener) {
List<String> currentValue = user.getAttributeStream(PHONE_NUMBER_VERIFIED).filter(Objects::nonNull).collect(Collectors.toList());

if (currentValue.stream().noneMatch(Boolean::parseBoolean)) {
return;
}

user.setAttribute(PHONE_NUMBER_VERIFIED, List.of(Boolean.FALSE.toString()));

for (AttributeChangeListener listener : changeListener) {
listener.onChange(PHONE_NUMBER_VERIFIED, user, currentValue);
}
}

private boolean isCustomAttribute(String name) {
return !isRootAttribute(name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,53 +42,55 @@ public enum UserProfileContext {
/**
* In this context, a user profile is managed by themselves during an authentication flow such as when updating the user profile.
*/
UPDATE_PROFILE(false, true, true),
UPDATE_PROFILE(false, true, true, true),

/**
* In this context, a user profile is managed through the management interface such as the Admin API.
*/
USER_API(true, false, false),
USER_API(true, false, false, false),

/**
* In this context, a user profile is managed by themselves through the account console.
*/
ACCOUNT(false, true, true),
ACCOUNT(false, true, true, true),

/**
* In this context, a user profile is managed by themselves when authenticating through a broker.
*/
IDP_REVIEW(false, true, false),
IDP_REVIEW(false, true, false, true),

/**
* In this context, a user profile is managed by themselves when registering to a realm.
*/
REGISTRATION(false, true, false),
REGISTRATION(false, true, false, false),

/**
* 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, true, Set.of(UserModel.EMAIL)::contains),
UPDATE_EMAIL(false, true, true, false, Set.of(UserModel.EMAIL)::contains),

/**
* In this context, a user profile is managed through the management interface such as the Admin API.
*/
SCIM(true, false, false);
SCIM(true, false, false, false);

private final boolean resetEmailVerified;
private final boolean resetPhoneNumberVerified;
private final Predicate<String> attributeSelector;
private final boolean adminContext;
private final boolean authFlowContext;

UserProfileContext(boolean adminContext, boolean authFlowContext, boolean resetEmailVerified, Predicate<String> attributeSelector){
UserProfileContext(boolean adminContext, boolean authFlowContext, boolean resetEmailVerified, boolean resetPhoneNumberVerified, Predicate<String> attributeSelector){
this.adminContext = adminContext;
this.authFlowContext = authFlowContext;
this.resetEmailVerified = resetEmailVerified;
this.resetPhoneNumberVerified = resetPhoneNumberVerified;
this.attributeSelector = attributeSelector;
}

UserProfileContext(boolean adminContext, boolean authFlowContext, boolean resetEmailVerified){
this(adminContext, authFlowContext, resetEmailVerified, StringUtil::isNotBlank);
UserProfileContext(boolean adminContext, boolean authFlowContext, boolean resetEmailVerified, boolean resetPhoneNumberVerified){
this(adminContext, authFlowContext, resetEmailVerified, resetPhoneNumberVerified, StringUtil::isNotBlank);
}

/**
Expand All @@ -112,6 +114,13 @@ public boolean isResetEmailVerified() {
return resetEmailVerified;
}

/**
* @return true means that the phoneNumberVerified attribute must be reset to false in this context when the phoneNumber attribute is updated
*/
public boolean isResetPhoneNumberVerified() {
return resetPhoneNumberVerified;
}

/**
* Check if roles configuration contains role for this context.
*
Expand Down
Loading
Loading
0