+
Skip to content

Password modification time attribute as an operational and read-only attribute #40280

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
Jun 10, 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
Original file line number Diff line number Diff line change
Expand Up @@ -1228,20 +1228,16 @@ public Stream<CredentialModel> getCredentials(RealmModel realm, UserModel user)
}

private long getPasswordChangedTime(LDAPObject ldapObject) {
String value = ldapObject.getAttributeAsString(getAttributeName());
String attributeName = getLdapIdentityStore().getPasswordModificationTimeAttributeName();
String value = ldapObject.getAttributeAsString(attributeName);

if (StringUtil.isBlank(value)) {
return -1L;
}

if (LDAPConstants.PWD_LAST_SET.equals(getAttributeName())) {
if (LDAPConstants.PWD_LAST_SET.equals(attributeName)) {
return (Long.parseLong(value) / 10000L) - 11644473600000L;
}
return LDAPUtils.generalizedTimeToDate(value).getTime();
}

private String getAttributeName() {
LDAPConfig config = getLdapIdentityStore().getConfig();
return config.isActiveDirectory() ? LDAPConstants.PWD_LAST_SET : LDAPConstants.PWD_CHANGED_TIME;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public static LDAPObject addUserToLDAP(LDAPStorageProvider ldapProvider, RealmMo
.collect(Collectors.toSet());
mandatoryAttrs.add(ldapConfig.getRdnLdapAttribute());

String passwordModifiedTimeAttributeName = ldapStore.getPasswordModificationTimeAttributeName();
String passwordModifiedTime = user.getFirstAttribute(passwordModifiedTimeAttributeName);
if (passwordModifiedTime != null) {
ldapUser.setSingleAttribute(passwordModifiedTimeAttributeName, passwordModifiedTime);
}

ldapUser.executeOnMandatoryAttributesComplete(mandatoryAttrs, ldapObject -> {
LDAPUtils.computeAndSetDn(ldapConfig, ldapObject);
ldapStore.add(ldapObject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,13 @@ protected BasicAttributes extractAttributesForSaving(LDAPObject ldapObject, bool
.map(String::toLowerCase)
.collect(Collectors.toSet());

if (!isCreate) {
// for updates, assume the PWD_CHANGED_TIME attribute is an operational attribute and read-only
// otherwise, updates will fail when trying to modify the attribute
// vendors like AD, support the same type of attribute differently and using a mapper
ldapObject.addReadOnlyAttributeName(LDAPConstants.PWD_CHANGED_TIME);
}

for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
String attrName = attrEntry.getKey();
Set<String> attrValue = attrEntry.getValue();
Expand Down Expand Up @@ -602,4 +609,8 @@ private BasicAttribute createBinaryBasicAttribute(String attrName, Set<String> a
return attr;
}

public String getPasswordModificationTimeAttributeName() {
return getConfig().isActiveDirectory() ? LDAPConstants.PWD_LAST_SET : LDAPConstants.PWD_CHANGED_TIME;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
public static final Set<String> PASSWORD_UPDATE_MSAD_ERROR_CODES = Set.of("52D");

private final Function<LDAPObject, UserAccountControl> GET_USER_ACCOUNT_CONTROL = ldapUser -> {
if (ldapUser == null) {
return UserAccountControl.empty();
}
String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL);
return UserAccountControl.of(userAccountControl);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ public class UserAccountControl {

private static final UserAccountControl EMPTY = new UserAccountControl(0);

public static UserAccountControl empty() {
return EMPTY;
}

public static UserAccountControl of(String userAccountControl) {
if (userAccountControl == null) {
return EMPTY;
return empty();
}
return new UserAccountControl(Long.parseLong(userAccountControl));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public String getFirstAttribute(String name) {
} else if (otherAttrs.containsKey(name)) {
return otherAttrs.getFirst(name);
}
return super.getFirstAttribute(name);
return null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,8 @@ public enum Encryption {
STARTTLS
}
}

public boolean isEmbeddedServer() {
return ldapTestConfiguration.isStartEmbeddedLdapServer();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserProfileResource;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.federation.kerberos.KerberosFederationProvider;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
Expand Down Expand Up @@ -90,17 +91,25 @@ protected LDAPRule getLDAPRule() {

@Override
protected void afterImportTestRealm() {
boolean isEmbeddedServer = ldapRule.isEmbeddedServer();
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();

// Delete all LDAP users and add some new for testing
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);

LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
john.setSingleAttribute(LDAPConstants.PWD_CHANGED_TIME, "22000101000000Z");
ctx.getLdapProvider().getLdapIdentityStore().update(john);
if (isEmbeddedServer) {
MultivaluedHashMap<String, String> otherAttrs = new MultivaluedHashMap<>();

otherAttrs.putSingle(LDAPConstants.PWD_CHANGED_TIME, "22000101000000Z");

LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", otherAttrs);
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
} else {
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public String getFirstAttribute(String name) {
} else if (UserModel.USERNAME.equals(name)) {
return username;
}
return super.getFirstAttribute(name);
return null;
}

@Override
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载