Skip to content

Commit

Permalink
Version 1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
zner0L committed Nov 29, 2021
1 parent 51b4735 commit 5e686d5
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 61 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Keycloak OpenLDAP ppolicy mapper

This is a plugin for the authentication provider [keycloak](https://keycloak.org). It maps the keycloak account state to OpenLDAP ppolicy attributes.
This is a plugin for the authentication provider [keycloak](https://keycloak.org). It maps the keycloak user's disabled state to the ppolicy `pwdAccountLockedTime` attribute. To properly work, the time set as `pwdLockoutDuration` in the password policy of the affected records should be set in the mapper settings.

**Warning:** This provider relies on private SPIs which may change at any point without notice. Please test the provider before you update your production deployment.

## Features

- Manually enable/disable users in OpenLDAP from Keycloak
- Disable users for the lockout duration if the password policy mandates it (e.g. too many dailed attempts)

## Deploy from source

1. To deploy from source, you must first build the plugin. You can use Maven to do so: `mvn clean package`.
2. Copy the target (from the `target` folder) into the `deployments` folder of your keycloak installation. (Typically: `/opt/keycloak/deployments`)

TODO
2. Copy the target (from the `target` folder) into the `deployments` folder of your keycloak installation. (Typically: `/opt/keycloak/standalone/deployments`)
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@

import javax.naming.AuthenticationException;

import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class OpenLDAPppolicyMapper extends AbstractLDAPStorageMapper {

private static final Logger logger = Logger.getLogger(OpenLDAPppolicyMapper.class);
public static final String LDAP_PPOLICY_LOCK_TIME = "pwdLockoutDuration";
public static final String LDAP_TIMESTAMP_FORMAT = "yyyyMMddkk[mm[ss]][.S]X";
public static final String LDAP_PPOLICY_LOCK_TIME = "pwdAccountLockedTime";
public static final String LDAP_TIMESTAMP_FORMAT = "yyyyMMddkkmmss[.SSS]X";
// this particular timestamp signifies a ermant block by the admin and can only
// be removed by the admin
public static final String LDAP_LOCKOUT_TIMESTAMP = "000001010000Z";
public static final String CONFIG_LDAP_LOCKOUT_DURATION = "ldap.ppolicy.lockout.duration";

public OpenLDAPppolicyMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
Expand All @@ -31,15 +34,7 @@ public OpenLDAPppolicyMapper(ComponentModel mapperModel, LDAPStorageProvider lda

@Override
public void beforeLDAPQuery(LDAPQuery query) {
query.addReturningLdapAttribute(LDAPConstants.PWD_LAST_SET);
query.addReturningLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);

// This needs to be read-only and can be set to writable just on demand
query.addReturningReadOnlyLdapAttribute(LDAPConstants.PWD_LAST_SET);

if (ldapProvider.getEditMode() != UserStorageProvider.EditMode.WRITABLE) {
query.addReturningReadOnlyLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
}
query.addReturningLdapAttribute(LDAP_PPOLICY_LOCK_TIME);
}

@Override
Expand All @@ -60,14 +55,20 @@ public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel
@Override
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException,
RealmModel realm) {
logger.debug(ldapException.getMessage());

/*
* String exceptionMessage = ldapException.getMessage(); Matcher m =
* AUTH_EXCEPTION_REGEX.matcher(exceptionMessage); if (m.matches()) { String
* errorCode = m.group(1); return processAuthErrorCode(errorCode, user); } else
* { return false; }
*/
long lockoutDuration = mapperModel.get(CONFIG_LDAP_LOCKOUT_DURATION, 0);

if (ldapException.getMessage().equals("[LDAP: error code 49 - Invalid Credentials]")) {
// OpenLDAP doesn't tell us in the error message if the Account is locked or the
// username/password are wrong, so we have to check ourselves
if (isLDAPUserLocked(ldapUser, lockoutDuration)) {
user.setEnabled(false);
return true;
} else {
user.setEnabled(true);
return false;
}
}

return false;
}

Expand All @@ -83,60 +84,63 @@ protected boolean processAuthErrorCode(String errorCode, UserModel user) {
return false;
}

public static boolean isLDAPUserLocked(LDAPObject ldapUser, long lockoutDuration) {
DateTimeFormatter ldapFormatter = DateTimeFormatter.ofPattern(LDAP_TIMESTAMP_FORMAT);
String lockTimestamp = ldapUser.getAttributeAsString(LDAP_PPOLICY_LOCK_TIME);
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));

if (lockTimestamp != null) {
if (lockTimestamp.equals(LDAP_LOCKOUT_TIMESTAMP)) {
return true;
} else {
ZonedDateTime lockedTime = ZonedDateTime.parse(lockTimestamp, ldapFormatter);

if (lockoutDuration > 0) {
ZonedDateTime unlockedTime = lockedTime.plusSeconds(lockoutDuration);
// account is only locked within the lockout interval
return lockedTime.isBefore(now) && unlockedTime.isAfter(now);
} else {
// lockoutDuration of 0 means the lockout is permanent until removed by the
// admin
return lockedTime.isBefore(now);
}
}
}

return false;
}

public class OpenLDAPUserModelDelegate extends TxAwareLDAPUserModelDelegate {

private final LDAPObject ldapUser;
private DateTimeFormatter ldapFormatter;

public OpenLDAPUserModelDelegate(UserModel delegate, LDAPObject ldapUser) {
super(delegate, ldapProvider, ldapUser);
this.ldapUser = ldapUser;
this.ldapFormatter = DateTimeFormatter.ofPattern(LDAP_TIMESTAMP_FORMAT);
}

@Override
public boolean isEnabled() {
boolean kcEnabled = super.isEnabled();
LocalDateTime lockedTime = getPwdLockedTime();
long lockoutDuration = mapperModel.get(CONFIG_LDAP_LOCKOUT_DURATION, 0);

if (lockedTime != null) {

if (lockoutDuration > 0) {
LocalDateTime unlockedTime = lockedTime.minusSeconds(lockoutDuration);
return kcEnabled && unlockedTime.isBefore(LocalDateTime.now(ZoneId.of("UTC")));
} else {
return kcEnabled && lockedTime.isAfter(LocalDateTime.now(ZoneId.of("UTC")));
}
} else {
return kcEnabled;
}
return super.isEnabled() && !isLDAPUserLocked(ldapUser, lockoutDuration);
}

@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
long lockoutDuration = mapperModel.get(CONFIG_LDAP_LOCKOUT_DURATION, 0);

if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE) {
OpenLDAPppolicyMapper.logger.debugf("Propagating enabled=%s for user '%s' to OpenLDAP", enabled,
ldapUser.getDn().toString());
if (!isLDAPUserLocked(ldapUser, lockoutDuration) != enabled) {
if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE) {

if (enabled) {
ldapUser.setSingleAttribute(LDAP_PPOLICY_LOCK_TIME, null);
} else {
ldapUser.setSingleAttribute(LDAP_PPOLICY_LOCK_TIME, "000001010000Z");
}
if (enabled) {
ldapUser.setAttribute(LDAP_PPOLICY_LOCK_TIME, null);
} else {
ldapUser.setSingleAttribute(LDAP_PPOLICY_LOCK_TIME, LDAP_LOCKOUT_TIMESTAMP);
}

markUpdatedAttributeInTransaction(LDAPConstants.ENABLED);
}
}

protected LocalDateTime getPwdLockedTime() {
String lockTimestamp = ldapUser.getAttributeAsString(LDAP_PPOLICY_LOCK_TIME);
if (lockTimestamp != null) {
return LocalDateTime.parse(lockTimestamp, this.ldapFormatter);
} else {
return null;
markUpdatedAttributeInTransaction(LDAPConstants.ENABLED);
}
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@

import java.util.List;

/**
* @author <a href="mailto:[email protected]">Marek Posolda</a>
*/
public class OpenLDAPppolicyMapperFactory extends AbstractLDAPStorageMapperFactory {

public static final String PROVIDER_ID = "openldap_ppolicy_mapper";
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/META-INF/jboss-deployment-structure.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-ldap-federation"/>
</dependencies>
</deployment>
</jboss-deployment-structure>

0 comments on commit 5e686d5

Please sign in to comment.