From 7028aa55dc96b7129692121974d564f68ab67ac8 Mon Sep 17 00:00:00 2001 From: Brian Long Date: Tue, 2 Nov 2021 10:55:20 -0400 Subject: [PATCH] fixes/refactoring after testing --- README.md | 14 ++++++++++---- pom.xml | 6 ++++++ ...oupExpiredPasswordRequiredActionFactory.java} | 9 +++++---- ...upExpiredPasswordRequiredActionProvider.java} | 13 ++++++++++--- .../GroupPasswordPolicyProviderFactory.java | 6 ++++++ .../policy/RequiredActionMultiplexer.java | 16 ++++++++++++++-- 6 files changed, 51 insertions(+), 13 deletions(-) rename src/main/java/com/github/jpicht/keycloak/policy/{GroupRequiredActionFactory.java => GroupExpiredPasswordRequiredActionFactory.java} (81%) rename src/main/java/com/github/jpicht/keycloak/policy/{GroupRequiredActionProvider.java => GroupExpiredPasswordRequiredActionProvider.java} (79%) diff --git a/README.md b/README.md index 943660b..b435012 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,17 @@ by navigating to the "Authentication" menu item in the vertical menu on the left realm's user interface. You will then need to navigate to the "Password Policy" tab along the menu of tabs on the top of the page. -This interface provides you the OOTB ability to specify password policies for all users. This -is still true with the plugin installed. You will also have an additional option: **Group +This interface provides you the OOTB ability to specify password policies for **all** users. +This is still true with the plugin installed. You will now have an additional option: **Group Policy**. To use the plugin, you must add that password policy. The "Policy Value" should be set to the ID we came up with earlier: `passwordPolicy`. +If you intend to use group-specific password expiration (`forceExpiredPasswordChange`), you will +need to perform an additional step in the configuration. In the same "Authentication" section, +navigate to the "Required Actions" tab along the menu of tabs on the top of the page. Use the +"Register" button to add the "Group-based Expired Password" action. Move it up in the order to +after the "Update Password" action. + At this point, you will need to add an attribute (with key `passwordPolicy`) to each group you want to have additional password policies. The format of that text is defined by Keycloak documentation and covered in the section below. @@ -50,7 +56,8 @@ The [policies provided with KeyCloak](https://www.keycloak.org/docs/6.0/server_a | `regexPattern(string)` | regular expression | ✓ | | `notUsername()` | | ✓ | | `passwordBlacklist(string)` | file name | - | -| `passwordHistory(int)` | number of last used passwords to disallow | - | +| `passwordHistory(int)` | number of last used passwords to disallow | ✓ | +| `forceExpiredPasswordChange(string)` | number of days to expire password after | ✓ | On the realm model the password policy attribute is also used for other purposes. There are some registered "policies", that do not actually implement a policy that @@ -60,7 +67,6 @@ If these currently work is completely untested. | Identifier | Description | Tested | | ------------- |:------------------------------------ | ------ | -| `forceExpiredPasswordChange(string)` | number of days to expire password after | - | | `hashAlgorithm(string)` | hash algorithm to use when hashing the password | - | | `hashIterations(int)` | number of hash iterations | - | diff --git a/pom.xml b/pom.xml index 163d564..a46bf3b 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,12 @@ maven-jar-plugin keycloak-v${keycloak.majorVersion} + + + + org.keycloak.keycloak-services + + diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupRequiredActionFactory.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionFactory.java similarity index 81% rename from src/main/java/com/github/jpicht/keycloak/policy/GroupRequiredActionFactory.java rename to src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionFactory.java index 4ab83cb..baaccc7 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/GroupRequiredActionFactory.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionFactory.java @@ -29,12 +29,12 @@ import com.google.auto.service.AutoService; * @author brian@inteligr8.com */ @AutoService(RequiredActionFactory.class) -public class GroupRequiredActionFactory implements RequiredActionFactory { +public class GroupExpiredPasswordRequiredActionFactory implements RequiredActionFactory { - private final Logger logger = Logger.getLogger(GroupRequiredActionFactory.class); + private final Logger logger = Logger.getLogger(GroupExpiredPasswordRequiredActionFactory.class); private static final String ID = "groupRequiredAction"; - private static final String DISPLAY = "Group Action"; + private static final String DISPLAY = "Group-based Expired Password"; @Override public String getId() { @@ -43,7 +43,8 @@ public class GroupRequiredActionFactory implements RequiredActionFactory { @Override public RequiredActionProvider create(KeycloakSession session) { - return new GroupRequiredActionProvider(session); + this.logger.trace("create()"); + return new GroupExpiredPasswordRequiredActionProvider(session); } @Override diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupRequiredActionProvider.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionProvider.java similarity index 79% rename from src/main/java/com/github/jpicht/keycloak/policy/GroupRequiredActionProvider.java rename to src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionProvider.java index 28c9ca5..350e28c 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/GroupRequiredActionProvider.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupExpiredPasswordRequiredActionProvider.java @@ -27,25 +27,31 @@ import org.keycloak.models.UserModel; /** * @author brian@inteligr8.com */ -public class GroupRequiredActionProvider extends RequiredActionMultiplexer { +public class GroupExpiredPasswordRequiredActionProvider extends RequiredActionMultiplexer { private final Logger logger = Logger.getLogger(RequiredActionMultiplexer.class); private final GroupPasswordPolicyFinder finder = new GroupPasswordPolicyFinder(); private final KeycloakSession session; - public GroupRequiredActionProvider(KeycloakSession session) { + public GroupExpiredPasswordRequiredActionProvider(KeycloakSession session) { this.session = session; } @Override protected int findDaysToExpire(RealmModel realm, UserModel user) { + if (this.logger.isTraceEnabled()) + this.logger.tracef("findDaysToExpire(%s, %s)", realm == null ? null : realm.getName(), user == null ? null : user.getId()); + List policyStrs = this.finder.findPolicies(realm, user); if (policyStrs == null || policyStrs.isEmpty()) return -1; + this.logger.debugf("found policies: [%s]", policyStrs.toString()); Integer minDaysToExpire = null; for (String policyStr : policyStrs) { + this.logger.tracef("inspecting policy: %s", policyStr); + PasswordPolicy policy = PasswordPolicy.parse(this.session, policyStr); int daysToExpire = policy.getDaysToExpirePassword(); if (daysToExpire < 0) @@ -62,7 +68,8 @@ public class GroupRequiredActionProvider extends RequiredActionMultiplexer { minDaysToExpire = Math.min(minDaysToExpire, daysToExpire); } } - + + this.logger.debugf("determined password expiration policy: %d days", minDaysToExpire); return minDaysToExpire == null ? -1 : minDaysToExpire; } diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java index 1ff1792..abaff5d 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProviderFactory.java @@ -17,6 +17,8 @@ package com.github.jpicht.keycloak.policy; import com.google.auto.service.AutoService; + +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -25,6 +27,8 @@ import org.keycloak.policy.PasswordPolicyProviderFactory; @AutoService(PasswordPolicyProviderFactory.class) public class GroupPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory { + + private final Logger logger = Logger.getLogger(GroupPasswordPolicyProviderFactory.class); static final String ID = "groupPasswordPolicy"; @@ -40,6 +44,7 @@ public class GroupPasswordPolicyProviderFactory implements PasswordPolicyProvide @Override public void init(Config.Scope config) { + this.logger.trace("init()"); } @Override @@ -68,5 +73,6 @@ public class GroupPasswordPolicyProviderFactory implements PasswordPolicyProvide @Override public void close() { + this.logger.trace("close()"); } } diff --git a/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java b/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java index 3584fea..ac82f5b 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java @@ -19,6 +19,7 @@ package com.github.jpicht.keycloak.policy; import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; +import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; @@ -37,6 +38,11 @@ abstract class RequiredActionMultiplexer implements RequiredActionProvider { private final Logger logger = Logger.getLogger(RequiredActionMultiplexer.class); protected abstract int findDaysToExpire(RealmModel realm, UserModel user); + + @Override + public InitiatedActionSupport initiatedActionSupport() { + return InitiatedActionSupport.SUPPORTED; + } /** * This is a re-implementation of what is found in the default @@ -51,20 +57,26 @@ abstract class RequiredActionMultiplexer implements RequiredActionProvider { int daysToExpirePassword = this.findDaysToExpire(context.getRealm(), context.getUser()); if (daysToExpirePassword > -1) { + this.logger.debugf("Found password expiration: %d days", daysToExpirePassword); + PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider)context.getSession() .getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID); CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser()); if (password != null) { + this.logger.tracef("Found password credentials; created: %d ms", password.getCreatedDate()); + if(password.getCreatedDate() == null) { context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - logger.debug("User is required to update password"); + this.logger.debug("User is required to update password"); } else { long timeElapsed = Time.currentTimeMillis() - password.getCreatedDate(); long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword); if(timeElapsed > timeToExpire) { context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - logger.debug("User is required to update password"); + this.logger.debug("User is required to update password"); + } else { + this.logger.tracef("Password credentials expire in %d ms", timeToExpire); } } }