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);
}
}
}