diff --git a/pom.xml b/pom.xml index 72f52ba..163d564 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,11 @@ jboss-logging 3.4.0.Final + + org.keycloak + keycloak-services + ${keycloak.version} + org.keycloak keycloak-core diff --git a/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java b/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java similarity index 67% rename from src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java rename to src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java index bc42684..b203f61 100644 --- a/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java +++ b/src/keycloak-v12-common/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java @@ -1,5 +1,6 @@ /* * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,29 +18,23 @@ package com.github.jpicht.keycloak.policy; import java.util.LinkedList; +import java.util.List; import java.util.function.Consumer; import org.jboss.logging.Logger; import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.policy.PasswordPolicyConfigException; -public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { +public class GroupPasswordPolicyFinder { - private static final Logger logger = Logger.getLogger(GroupPasswordPolicyProvider.class); + private static final Logger logger = Logger.getLogger(GroupPasswordPolicyFinder.class); - public GroupPasswordPolicyProvider(KeycloakSession session) { - super(session); - } - - @Override - protected LinkedList findPolicies(RealmModel realm, UserModel user) { + public List findPolicies(RealmModel realm, UserModel user) { // First get the name of the attribute String groupAttribute = realm.getPasswordPolicy().getPolicyConfig(GroupPasswordPolicyProviderFactory.ID); - logger.debugf("groupAttribute %s", groupAttribute); - logger.debugf("user %s", user.getUsername()); + logger.debugf("groupAttribute: %s", groupAttribute); + logger.debugf("user: %s", user.getUsername()); LinkedList policyDefinitions = new LinkedList<>(); @@ -47,7 +42,7 @@ public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { user.getGroupsStream().forEach(new Consumer() { @Override public void accept(GroupModel group) { - logger.debugf("group %s", group.getName()); + logger.debugf("group: %s", group.getName()); group.getAttributeStream(groupAttribute).forEach(new Consumer() { @Override public void accept(String policyString) { @@ -60,16 +55,5 @@ public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { return policyDefinitions; } - - @Override - public Object parseConfig(String value) { - if (value == null || value.isEmpty()) { - throw new PasswordPolicyConfigException("Attribute name cannot be blank"); - } - return value; - } - - @Override - public void close() { - } + } diff --git a/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java b/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java similarity index 63% rename from src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java rename to src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java index 0da4575..5c1f41e 100644 --- a/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java +++ b/src/keycloak-v6-v11/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyFinder.java @@ -17,33 +17,28 @@ package com.github.jpicht.keycloak.policy; import java.util.LinkedList; +import java.util.List; + import org.jboss.logging.Logger; import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.policy.PasswordPolicyConfigException; -public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { +public class GroupPasswordPolicyFinder { - private static final Logger logger = Logger.getLogger(GroupPasswordPolicyProvider.class); + private static final Logger logger = Logger.getLogger(GroupPasswordPolicyFinder.class); - public GroupPasswordPolicyProvider(KeycloakSession session) { - super(session); - } - - @Override - protected LinkedList findPolicies(RealmModel realm, UserModel user) { + protected List findPolicies(RealmModel realm, UserModel user) { // First get the name of the attribute String groupAttribute = realm.getPasswordPolicy().getPolicyConfig(GroupPasswordPolicyProviderFactory.ID); - logger.debugf("groupAttribute %s", groupAttribute); - logger.debugf("user %s", user.getUsername()); + logger.debugf("groupAttribute: %s", groupAttribute); + logger.debugf("user: %s", user.getUsername()); LinkedList policyDefinitions = new LinkedList<>(); // Iterate groups and collect policy strings for (GroupModel group : user.getGroups()) { - logger.debugf("group %s", group.getName()); + logger.debugf("group: %s", group.getName()); for (String policyString : group.getAttribute(groupAttribute)) { logger.infof("adding group password policy: %s", policyString); policyDefinitions.add(policyString); @@ -52,16 +47,5 @@ public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { return policyDefinitions; } - - @Override - public Object parseConfig(String value) { - if (value == null || value.isEmpty()) { - throw new PasswordPolicyConfigException("Attribute name cannot be blank"); - } - return value; - } - - @Override - public void close() { - } + } diff --git a/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java new file mode 100644 index 0000000..eee8b76 --- /dev/null +++ b/src/main/java/com/github/jpicht/keycloak/policy/GroupPasswordPolicyProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Julian Picht + * Copyright 2021 Brian Long + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.jpicht.keycloak.policy; + +import java.util.List; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.policy.PasswordPolicyConfigException; + +public class GroupPasswordPolicyProvider extends PolicyProviderMultiplexer { + + private GroupPasswordPolicyFinder finder = new GroupPasswordPolicyFinder(); + + public GroupPasswordPolicyProvider(KeycloakSession session) { + super(session); + } + + @Override + protected List findPolicies(RealmModel realm, UserModel user) { + return this.finder.findPolicies(realm, user); + } + + @Override + public Object parseConfig(String value) { + if (value == null || value.isEmpty()) { + throw new PasswordPolicyConfigException("Attribute name cannot be blank"); + } + return value; + } + + @Override + public void close() { + } + +} diff --git a/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java b/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java index f6b199d..fa0bb8e 100644 --- a/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java +++ b/src/main/java/com/github/jpicht/keycloak/policy/PolicyProviderMultiplexer.java @@ -39,7 +39,7 @@ abstract public class PolicyProviderMultiplexer implements PasswordPolicyProvide this.session = session; } - abstract protected LinkedList findPolicies(RealmModel realm, UserModel user); + protected abstract List findPolicies(RealmModel realm, UserModel user); @Override public PolicyError validate(String username, String password) { diff --git a/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java b/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java new file mode 100644 index 0000000..f412212 --- /dev/null +++ b/src/main/java/com/github/jpicht/keycloak/policy/RequiredActionMultiplexer.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Brian Long + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.jpicht.keycloak.policy; + +import java.util.concurrent.TimeUnit; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.common.util.Time; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.PasswordCredentialProvider; +import org.keycloak.credential.PasswordCredentialProviderFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +/** + * @author brian@inteligr8.com + */ +abstract class RequiredActionMultiplexer implements RequiredActionProvider { + + private final Logger logger = Logger.getLogger(RequiredActionMultiplexer.class); + + protected abstract int findDaysToExpire(RealmModel realm, UserModel user); + + /** + * This is a re-implementation of what is found in the default + * implementation in Keycloak. It just makes days-to-expire abstract so it + * can be implemented any number of ways. + * + * https://github.com/keycloak/keycloak/blob/af3b573d196af882dfb25cdccb98361746e85481/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java#L67 + */ + @Override + public void evaluateTriggers(RequiredActionContext context) { + this.logger.tracef("evaluateTriggers({})", context.getUser() != null ? context.getUser().getUsername() : null); + + int daysToExpirePassword = this.findDaysToExpire(context.getRealm(), context.getUser()); + if (daysToExpirePassword > -1) { + PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider)context.getSession() + .getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID); + CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser()); + if (password != null) { + if(password.getCreatedDate() == null) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + 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"); + } + } + } + } + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + this.logger.tracef("requiredActionChallenge({})", context.getUser() != null ? context.getUser().getUsername() : null); + } + + @Override + public void processAction(RequiredActionContext context) { + this.logger.tracef("processAction({})", context.getUser() != null ? context.getUser().getUsername() : null); + } + +}