Merge pull request #3030 from stianst/KEYCLOAK-2824-2

KEYCLOAK-2824 Password Policy SPI
This commit is contained in:
Stian Thorgersen 2016-07-14 10:12:25 +02:00 committed by GitHub
commit 4f1d83b9dc
58 changed files with 2285 additions and 796 deletions

View file

@ -0,0 +1,70 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordPolicyTypeRepresentation {
private String id;
private String displayName;
private String configType;
private String defaultValue;
private boolean multipleSupported;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getConfigType() {
return configType;
}
public void setConfigType(String configType) {
this.configType = configType;
}
public String getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
public boolean isMultipleSupported() {
return multipleSupported;
}
public void setMultipleSupported(boolean multipleSupported) {
this.multipleSupported = multipleSupported;
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.representations.info; package org.keycloak.representations.info;
import org.keycloak.representations.idm.PasswordPolicyTypeRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
@ -43,6 +44,8 @@ public class ServerInfoRepresentation {
private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers; private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers;
private Map<String, List<ClientInstallationRepresentation>> clientInstallations; private Map<String, List<ClientInstallationRepresentation>> clientInstallations;
private List<PasswordPolicyTypeRepresentation> passwordPolicies;
private Map<String, List<String>> enums; private Map<String, List<String>> enums;
public SystemInfoRepresentation getSystemInfo() { public SystemInfoRepresentation getSystemInfo() {
@ -132,4 +135,13 @@ public class ServerInfoRepresentation {
public void setClientInstallations(Map<String, List<ClientInstallationRepresentation>> clientInstallations) { public void setClientInstallations(Map<String, List<ClientInstallationRepresentation>> clientInstallations) {
this.clientInstallations = clientInstallations; this.clientInstallations = clientInstallations;
} }
public List<PasswordPolicyTypeRepresentation> getPasswordPolicies() {
return passwordPolicies;
}
public void setPasswordPolicies(List<PasswordPolicyTypeRepresentation> passwordPolicies) {
this.passwordPolicies = passwordPolicies;
}
} }

View file

@ -1223,7 +1223,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override @Override
public PasswordPolicy getPasswordPolicy() { public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) { if (passwordPolicy == null) {
passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy()); passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
} }
return passwordPolicy; return passwordPolicy;
} }

View file

@ -308,7 +308,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override @Override
public PasswordPolicy getPasswordPolicy() { public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) { if (passwordPolicy == null) {
passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy()); passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
} }
return passwordPolicy; return passwordPolicy;
} }

View file

@ -18,7 +18,11 @@
package org.keycloak.hash; package org.keycloak.hash;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.*; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -32,17 +36,12 @@ public class PasswordHashManager {
} }
public static UserCredentialValueModel encode(KeycloakSession session, PasswordPolicy passwordPolicy, String rawPassword) { public static UserCredentialValueModel encode(KeycloakSession session, PasswordPolicy passwordPolicy, String rawPassword) {
String algorithm = passwordPolicy.getHashAlgorithm();
int iterations = passwordPolicy.getHashIterations();
if (iterations < 1) {
iterations = 1;
}
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, passwordPolicy.getHashAlgorithm()); PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, passwordPolicy.getHashAlgorithm());
if (provider == null) { if (provider == null) {
log.warnv("Could not find hash provider {0} from password policy, using default provider {1}", algorithm, Constants.DEFAULT_HASH_ALGORITHM); log.warnv("Could not find hash provider {0} from password policy, using default provider {1}", passwordPolicy.getHashAlgorithm(), HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE);
provider = session.getProvider(PasswordHashProvider.class, Constants.DEFAULT_HASH_ALGORITHM); provider = session.getProvider(PasswordHashProvider.class, HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE);
} }
return provider.encode(rawPassword, iterations); return provider.encode(rawPassword, passwordPolicy.getHashIterations());
} }
public static boolean verify(KeycloakSession session, RealmModel realm, String password, UserCredentialValueModel credential) { public static boolean verify(KeycloakSession session, RealmModel realm, String password, UserCredentialValueModel credential) {

View file

@ -41,8 +41,6 @@ public interface Constants {
String AUTHZ_UMA_AUTHORIZATION = "uma_authorization"; String AUTHZ_UMA_AUTHORIZATION = "uma_authorization";
String[] AUTHZ_DEFAULT_AUTHORIZATION_ROLES = {AUTHZ_UMA_AUTHORIZATION}; String[] AUTHZ_DEFAULT_AUTHORIZATION_ROLES = {AUTHZ_UMA_AUTHORIZATION};
String DEFAULT_HASH_ALGORITHM = "pbkdf2";
// 15 minutes // 15 minutes
int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900; int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
// 30 days // 30 days

View file

@ -17,498 +17,101 @@
package org.keycloak.models; package org.keycloak.models;
import org.keycloak.hash.PasswordHashManager; import org.keycloak.policy.ForceExpiredPasswordPolicyProviderFactory;
import org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory;
import org.keycloak.policy.HashIterationsPasswordPolicyProviderFactory;
import org.keycloak.policy.HistoryPasswordPolicyProviderFactory;
import org.keycloak.policy.PasswordPolicyProvider;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.HashMap;
import java.util.Collections; import java.util.Map;
import java.util.Comparator; import java.util.Set;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class PasswordPolicy implements Serializable { public class PasswordPolicy implements Serializable {
public static final String INVALID_PASSWORD_MIN_LENGTH_MESSAGE = "invalidPasswordMinLengthMessage";
public static final String INVALID_PASSWORD_MIN_DIGITS_MESSAGE = "invalidPasswordMinDigitsMessage";
public static final String INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE = "invalidPasswordMinLowerCaseCharsMessage";
public static final String INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE = "invalidPasswordMinUpperCaseCharsMessage";
public static final String INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE = "invalidPasswordMinSpecialCharsMessage";
public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage";
public static final String INVALID_PASSWORD_REGEX_PATTERN = "invalidPasswordRegexPatternMessage";
public static final String INVALID_PASSWORD_HISTORY = "invalidPasswordHistoryMessage";
private List<Policy> policies;
private String policyString; private String policyString;
private Map<String, Object> policyConfig;
public PasswordPolicy(String policyString) { public static PasswordPolicy empty() {
this.policyString = policyString; return new PasswordPolicy(null, new HashMap<>());
this.policies = new LinkedList<>(); }
public static PasswordPolicy parse(KeycloakSession session, String policyString) {
Map<String, Object> policyConfig = new HashMap<>();
if (policyString != null && !policyString.trim().isEmpty()) { if (policyString != null && !policyString.trim().isEmpty()) {
for (String policy : policyString.split(" and ")) { for (String policy : policyString.split(" and ")) {
policy = policy.trim(); policy = policy.trim();
String name; String key;
String arg = null; String config = null;
int i = policy.indexOf('('); int i = policy.indexOf('(');
if (i == -1) { if (i == -1) {
name = policy.trim(); key = policy.trim();
} else { } else {
name = policy.substring(0, i).trim(); key = policy.substring(0, i).trim();
arg = policy.substring(i + 1, policy.length() - 1); config = policy.substring(i + 1, policy.length() - 1);
} }
if (name.equals(Length.NAME)) { PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, key);
policies.add(new Length(arg)); if (provider == null) {
} else if (name.equals(Digits.NAME)) {
policies.add(new Digits(arg));
} else if (name.equals(LowerCase.NAME)) {
policies.add(new LowerCase(arg));
} else if (name.equals(UpperCase.NAME)) {
policies.add(new UpperCase(arg));
} else if (name.equals(SpecialChars.NAME)) {
policies.add(new SpecialChars(arg));
} else if (name.equals(NotUsername.NAME)) {
policies.add(new NotUsername(arg));
} else if (name.equals(HashAlgorithm.NAME)) {
policies.add(new HashAlgorithm(arg));
} else if (name.equals(HashIterations.NAME)) {
policies.add(new HashIterations(arg));
} else if (name.equals(RegexPatterns.NAME)) {
Pattern.compile(arg);
policies.add(new RegexPatterns(arg));
} else if (name.equals(PasswordHistory.NAME)) {
policies.add(new PasswordHistory(arg, this));
} else if (name.equals(ForceExpiredPasswordChange.NAME)) {
policies.add(new ForceExpiredPasswordChange(arg));
} else {
throw new IllegalArgumentException("Unsupported policy"); throw new IllegalArgumentException("Unsupported policy");
} }
policyConfig.put(key, provider.parseConfig(config));
} }
} }
return new PasswordPolicy(policyString, policyConfig);
}
private PasswordPolicy(String policyString, Map<String, Object> policyConfig) {
this.policyString = policyString;
this.policyConfig = policyConfig;
}
public Set<String> getPolicies() {
return policyConfig.keySet();
}
public <T> T getPolicyConfig(String key) {
return (T) policyConfig.get(key);
} }
public String getHashAlgorithm() { public String getHashAlgorithm() {
if (policies == null) if (policyConfig.containsKey(HashAlgorithmPasswordPolicyProviderFactory.ID)) {
return Constants.DEFAULT_HASH_ALGORITHM; return getPolicyConfig(HashAlgorithmPasswordPolicyProviderFactory.ID);
for (Policy p : policies) { } else {
if (p instanceof HashAlgorithm) { return HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE;
return ((HashAlgorithm) p).algorithm;
}
} }
return Constants.DEFAULT_HASH_ALGORITHM;
} }
/**
*
* @return -1 if no hash iterations setting
*/
public int getHashIterations() { public int getHashIterations() {
if (policies == null) if (policyConfig.containsKey(HashIterationsPasswordPolicyProviderFactory.ID)) {
return -1; return getPolicyConfig(HashIterationsPasswordPolicyProviderFactory.ID);
for (Policy p : policies) { } else {
if (p instanceof HashIterations) { return HashIterationsPasswordPolicyProviderFactory.DEFAULT_VALUE;
return ((HashIterations) p).iterations;
}
} }
return -1;
} }
/**
*
* @return -1 if no expired passwords setting
*/
public int getExpiredPasswords() { public int getExpiredPasswords() {
if (policies == null) if (policyConfig.containsKey(HistoryPasswordPolicyProviderFactory.ID)) {
return getPolicyConfig(HistoryPasswordPolicyProviderFactory.ID);
} else {
return -1; return -1;
for (Policy p : policies) {
if (p instanceof PasswordHistory) {
return ((PasswordHistory) p).passwordHistoryPolicyValue;
}
}
return -1;
}
/**
*
* @return -1 if no force expired password change setting
*/
public int getDaysToExpirePassword() {
if (policies == null)
return -1;
for (Policy p : policies) {
if (p instanceof ForceExpiredPasswordChange) {
return ((ForceExpiredPasswordChange) p).daysToExpirePassword;
}
}
return -1;
}
public Error validate(KeycloakSession session, UserModel user, String password) {
for (Policy p : policies) {
Error error = p.validate(session, user, password);
if (error != null) {
return error;
}
}
return null;
}
public Error validate(KeycloakSession session, String user, String password) {
for (Policy p : policies) {
Error error = p.validate(session, user, password);
if (error != null) {
return error;
}
}
return null;
}
private static interface Policy extends Serializable {
public Error validate(KeycloakSession session, UserModel user, String password);
public Error validate(KeycloakSession session, String user, String password);
}
public static class Error {
private String message;
private Object[] parameters;
private Error(String message, Object... parameters) {
this.message = message;
this.parameters = parameters;
}
public String getMessage() {
return message;
}
public Object[] getParameters() {
return parameters;
} }
} }
private static class HashAlgorithm implements Policy { public int getDaysToExpirePassword() {
private static final String NAME = "hashAlgorithm"; if (policyConfig.containsKey(ForceExpiredPasswordPolicyProviderFactory.ID)) {
private String algorithm; return getPolicyConfig(ForceExpiredPasswordPolicyProviderFactory.ID);
public HashAlgorithm(String arg) {
algorithm = stringArg(NAME, Constants.DEFAULT_HASH_ALGORITHM, arg);
}
@Override
public Error validate(KeycloakSession session, String user, String password) {
return null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return null;
}
}
private static class HashIterations implements Policy {
private static final String NAME = "hashIterations";
private int iterations;
public HashIterations(String arg) {
iterations = intArg(NAME, 1, arg);
}
@Override
public Error validate(KeycloakSession session, String user, String password) {
return null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return null;
}
}
private static class NotUsername implements Policy {
private static final String NAME = "notUsername";
public NotUsername(String arg) {
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class Length implements Policy {
private static final String NAME = "length";
private int min;
public Length(String arg)
{
min = intArg(NAME, 8, arg);
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class Digits implements Policy {
private static final String NAME = "digits";
private int min;
public Digits(String arg)
{
min = intArg(NAME, 1, arg);
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isDigit(c)) {
count++;
}
}
return count < min ? new Error(INVALID_PASSWORD_MIN_DIGITS_MESSAGE, min) : null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class LowerCase implements Policy {
private static final String NAME = "lowerCase";
private int min;
public LowerCase(String arg)
{
min = intArg(NAME, 1, arg);
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isLowerCase(c)) {
count++;
}
}
return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class UpperCase implements Policy {
private static final String NAME = "upperCase";
private int min;
public UpperCase(String arg) {
min = intArg(NAME, 1, arg);
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) {
count++;
}
}
return count < min ? new Error(INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class SpecialChars implements Policy {
private static final String NAME = "specialChars";
private int min;
public SpecialChars(String arg)
{
min = intArg(NAME, 1, arg);
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (!Character.isLetterOrDigit(c)) {
count++;
}
}
return count < min ? new Error(INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class RegexPatterns implements Policy {
private static final String NAME = "regexPattern";
private String regexPattern;
public RegexPatterns(String arg)
{
regexPattern = arg;
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
Pattern pattern = Pattern.compile(regexPattern);
Matcher matcher = pattern.matcher(password);
if (!matcher.matches()) {
return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object) regexPattern);
}
return null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
private static class PasswordHistory implements Policy {
private static final String NAME = "passwordHistory";
private final PasswordPolicy passwordPolicy;
private int passwordHistoryPolicyValue;
public PasswordHistory(String arg, PasswordPolicy passwordPolicy)
{
this.passwordPolicy = passwordPolicy;
passwordHistoryPolicyValue = intArg(NAME, 3, arg);
}
@Override
public Error validate(KeycloakSession session, String user, String password) {
return null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
if (passwordHistoryPolicyValue != -1) {
UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
if (cred != null) {
if(PasswordHashManager.verify(session, passwordPolicy, password, cred)) {
return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
}
}
List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
UserCredentialModel.PASSWORD_HISTORY);
for (UserCredentialValueModel credential : passwordExpiredCredentials) {
if (PasswordHashManager.verify(session, passwordPolicy, password, credential)) {
return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
}
}
}
return null;
}
private UserCredentialValueModel getCredentialValueModel(UserModel user, String credType) {
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
return model;
}
}
return null;
}
private List<UserCredentialValueModel> getCredentialValueModels(UserModel user, int expiredPasswordsPolicyValue,
String credType) {
List<UserCredentialValueModel> credentialModels = new ArrayList<UserCredentialValueModel>();
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
credentialModels.add(model);
}
}
Collections.sort(credentialModels, new Comparator<UserCredentialValueModel>() {
public int compare(UserCredentialValueModel credFirst, UserCredentialValueModel credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
if (credentialModels.size() > expiredPasswordsPolicyValue) {
return credentialModels.subList(0, expiredPasswordsPolicyValue);
}
return credentialModels;
}
}
private static class ForceExpiredPasswordChange implements Policy {
private static final String NAME = "forceExpiredPasswordChange";
private int daysToExpirePassword;
public ForceExpiredPasswordChange(String arg) {
daysToExpirePassword = intArg(NAME, 365, arg);
}
@Override
public Error validate(KeycloakSession session, String username, String password) {
return null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return null;
}
}
private static int intArg(String policy, int defaultValue, String arg) {
if (arg == null) {
return defaultValue;
} else { } else {
return Integer.parseInt(arg); return -1;
}
}
private static String stringArg(String policy, String defaultValue, String arg) {
if (arg == null) {
return defaultValue;
} else {
return arg;
} }
} }
@ -516,4 +119,5 @@ public class PasswordPolicy implements Serializable {
public String toString() { public String toString() {
return policyString; return policyString;
} }
} }

View file

@ -19,6 +19,8 @@ package org.keycloak.models;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
import org.keycloak.services.managers.UserManager; import org.keycloak.services.managers.UserManager;
import org.keycloak.storage.StorageProviderModel; import org.keycloak.storage.StorageProviderModel;
@ -493,7 +495,7 @@ public class UserFederationManager implements UserProvider {
public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) { public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
if (credential.getType().equals(UserCredentialModel.PASSWORD)) { if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
if (realm.getPasswordPolicy() != null) { if (realm.getPasswordPolicy() != null) {
PasswordPolicy.Error error = realm.getPasswordPolicy().validate(session, user, credential.getValue()); PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(user, credential.getValue());
if (error != null) throw new ModelException(error.getMessage(), error.getParameters()); if (error != null) throw new ModelException(error.getMessage(), error.getParameters());
} }
} }

View file

@ -17,19 +17,18 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.keycloak.common.util.Time;
import org.keycloak.hash.PasswordHashManager; import org.keycloak.hash.PasswordHashManager;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.representations.PasswordToken; import org.keycloak.representations.PasswordToken;
import org.keycloak.common.util.Time;
import java.util.List; import java.util.List;
@ -39,15 +38,6 @@ import java.util.List;
*/ */
public class CredentialValidation { public class CredentialValidation {
private static int hashIterations(RealmModel realm) {
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
return policy.getHashIterations();
}
return -1;
}
/** /**
* Will update password if hash iteration policy has changed * Will update password if hash iteration policy has changed
* *
@ -78,8 +68,7 @@ public class CredentialValidation {
boolean validated = PasswordHashManager.verify(session, realm, unhashedCredValue, credential); boolean validated = PasswordHashManager.verify(session, realm, unhashedCredValue, credential);
if (validated) { if (validated) {
int iterations = hashIterations(realm); if (realm.getPasswordPolicy().getHashIterations() != credential.getHashIterations()) {
if (iterations > -1 && iterations != credential.getHashIterations()) {
UserCredentialValueModel newCred = PasswordHashManager.encode(session, realm, unhashedCredValue); UserCredentialValueModel newCred = PasswordHashManager.encode(session, realm, unhashedCredValue);
user.updateCredentialDirectly(newCred); user.updateCredentialDirectly(newCred);

View file

@ -196,7 +196,7 @@ public class RepresentationToModel {
newRealm.addRequiredCredential(CredentialRepresentation.PASSWORD); newRealm.addRequiredCredential(CredentialRepresentation.PASSWORD);
} }
if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep)); if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY); else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
@ -661,7 +661,7 @@ public class RepresentationToModel {
return url != null ? url.replace(target, replacement) : null; return url != null ? url.replace(target, replacement) : null;
} }
public static void updateRealm(RealmRepresentation rep, RealmModel realm) { public static void updateRealm(RealmRepresentation rep, RealmModel realm, KeycloakSession session) {
if (rep.getRealm() != null) { if (rep.getRealm() != null) {
renameRealm(realm, rep.getRealm()); renameRealm(realm, rep.getRealm());
} }
@ -709,7 +709,7 @@ public class RepresentationToModel {
if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled());
if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep)); if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep));
if (rep.getDefaultRoles() != null) { if (rep.getDefaultRoles() != null) {

View file

@ -0,0 +1,74 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.UserModel;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultPasswordPolicyManagerProvider implements PasswordPolicyManagerProvider {
private KeycloakSession session;
public DefaultPasswordPolicyManagerProvider(KeycloakSession session) {
this.session = session;
}
@Override
public PolicyError validate(UserModel user, String password) {
for (PasswordPolicyProvider p : getProviders(session)) {
PolicyError policyError = p.validate(user, password);
if (policyError != null) {
return policyError;
}
}
return null;
}
@Override
public PolicyError validate(String user, String password) {
for (PasswordPolicyProvider p : getProviders(session)) {
PolicyError policyError = p.validate(user, password);
if (policyError != null) {
return policyError;
}
}
return null;
}
@Override
public void close() {
}
private List<PasswordPolicyProvider> getProviders(KeycloakSession session) {
LinkedList<PasswordPolicyProvider> list = new LinkedList<>();
PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
for (String id : policy.getPolicies()) {
PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, id);
list.add(provider);
}
return list;
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultPasswordPolicyManagerProviderFactory implements PasswordPolicyManagerProviderFactory {
@Override
public PasswordPolicyManagerProvider create(KeycloakSession session) {
return new DefaultPasswordPolicyManagerProvider(session);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DigitsPasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordMinDigitsMessage";
private KeycloakContext context;
public DigitsPasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
int min = context.getRealm().getPasswordPolicy().getPolicyConfig(DigitsPasswordPolicyProviderFactory.ID);
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isDigit(c)) {
count++;
}
}
return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DigitsPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
static final String ID = "digits";
@Override
public String getId() {
return ID;
}
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new DigitsPasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getDisplayName() {
return "Digits";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return "1";
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ForceExpiredPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory, PasswordPolicyProvider {
public static final String ID = "forceExpiredPasswordChange";
public static final int DEFAULT_VALUE = 365;
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
@Override
public PolicyError validate(UserModel user, String password) {
return null;
}
@Override
public PolicyError validate(String user, String password) {
return null;
}
@Override
public String getDisplayName() {
return "Expire Password";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.STRING_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return String.valueOf(DEFAULT_VALUE);
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : DEFAULT_VALUE;
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HashAlgorithmPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory, PasswordPolicyProvider {
public static final String DEFAULT_VALUE = "pbkdf2";
public static final String ID = "hashAlgorithm";
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
@Override
public PolicyError validate(UserModel user, String password) {
return null;
}
@Override
public PolicyError validate(String user, String password) {
return null;
}
@Override
public String getDisplayName() {
return "Hashing Algorithm";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.STRING_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return DEFAULT_VALUE;
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public Object parseConfig(String value) {
return value != null ? value : DEFAULT_VALUE;
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HashIterationsPasswordPolicyProviderFactory implements PasswordPolicyProvider, PasswordPolicyProviderFactory {
public static final int DEFAULT_VALUE = 20000;
public static final String ID = "hashIterations";
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return ID;
}
@Override
public PolicyError validate(UserModel user, String password) {
return null;
}
@Override
public PolicyError validate(String user, String password) {
return null;
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : DEFAULT_VALUE;
}
@Override
public String getDisplayName() {
return "Hashing Iterations";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return String.valueOf(DEFAULT_VALUE);
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordHistoryMessage";
private KeycloakSession session;
public HistoryPasswordPolicyProvider(KeycloakSession session) {
this.session = session;
}
@Override
public PolicyError validate(String username, String password) {
return null;
}
@Override
public PolicyError validate(UserModel user, String password) {
PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
int passwordHistoryPolicyValue = policy.getPolicyConfig(HistoryPasswordPolicyProviderFactory.ID);
if (passwordHistoryPolicyValue != -1) {
UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
if (cred != null) {
if(PasswordHashManager.verify(session, policy, password, cred)) {
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
}
}
List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
UserCredentialModel.PASSWORD_HISTORY);
for (UserCredentialValueModel credential : passwordExpiredCredentials) {
if (PasswordHashManager.verify(session, policy, password, credential)) {
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
}
}
}
return null;
}
private UserCredentialValueModel getCredentialValueModel(UserModel user, String credType) {
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
return model;
}
}
return null;
}
private List<UserCredentialValueModel> getCredentialValueModels(UserModel user, int expiredPasswordsPolicyValue, String credType) {
List<UserCredentialValueModel> credentialModels = new ArrayList<UserCredentialValueModel>();
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
credentialModels.add(model);
}
}
Collections.sort(credentialModels, new Comparator<UserCredentialValueModel>() {
public int compare(UserCredentialValueModel credFirst, UserCredentialValueModel credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
if (credentialModels.size() > expiredPasswordsPolicyValue) {
return credentialModels.subList(0, expiredPasswordsPolicyValue);
}
return credentialModels;
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HistoryPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
public static final String ID = "passwordHistory";
public static final Integer DEFAULT_VALUE = 3;
@Override
public String getId() {
return ID;
}
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new HistoryPasswordPolicyProvider(session);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getDisplayName() {
return "Not Recently Used";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return String.valueOf(HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE);
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LengthPasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordMinLengthMessage";
private KeycloakContext context;
public LengthPasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
int min = context.getRealm().getPasswordPolicy().getPolicyConfig(LengthPasswordPolicyProviderFactory.ID);
return password.length() < min ? new PolicyError(ERROR_MESSAGE, min) : null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 8;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LengthPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
static final String ID = "length";
@Override
public String getId() {
return ID;
}
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new LengthPasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getDisplayName() {
return "Minimum Length";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return "8";
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LowerCasePasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordMinLowerCaseCharsMessage";
private KeycloakContext context;
public LowerCasePasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
int min = context.getRealm().getPasswordPolicy().getPolicyConfig(LowerCasePasswordPolicyProviderFactory.ID);
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isLowerCase(c)) {
count++;
}
}
return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LowerCasePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
public static final String ID = "lowerCase";
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new LowerCasePasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getDisplayName() {
return "Lowercase Characters";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return "1";
}
@Override
public void close() {
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public String getId() {
return ID;
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class NotUsernamePasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordNotUsernameMessage";
private KeycloakContext context;
public NotUsernamePasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
return username.equals(password) ? new PolicyError(ERROR_MESSAGE) : null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
return null;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class NotUsernamePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
static final String ID = "notUsername";
@Override
public String getId() {
return ID;
}
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new NotUsernamePasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getDisplayName() {
return "Not Username";
}
@Override
public String getConfigType() {
return null;
}
@Override
public String getDefaultConfigValue() {
return null;
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
*/
public interface PasswordPolicyManagerProvider extends Provider {
PolicyError validate(UserModel user, String password);
PolicyError validate(String user, String password);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
*/
public interface PasswordPolicyManagerProviderFactory extends ProviderFactory<PasswordPolicyManagerProvider> {
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordPolicyManagerSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "password-policy-manager";
}
@Override
public Class<? extends Provider> getProviderClass() {
return PasswordPolicyManagerProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return PasswordPolicyManagerProviderFactory.class;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
*/
public interface PasswordPolicyProvider extends Provider {
String STRING_CONFIG_TYPE = "String";
String INT_CONFIG_TYPE = "int";
PolicyError validate(UserModel user, String password);
PolicyError validate(String user, String password);
Object parseConfig(String value);
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
*/
public interface PasswordPolicyProviderFactory extends ProviderFactory<PasswordPolicyProvider> {
String getDisplayName();
String getConfigType();
String getDefaultConfigValue();
boolean isMultiplSupported();
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
*/
public class PasswordPolicySpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "password-policy";
}
@Override
public Class<? extends Provider> getProviderClass() {
return PasswordPolicyProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return PasswordPolicyProviderFactory.class;
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public final class PolicyError {
private String message;
private Object[] parameters;
public PolicyError(String message, Object... parameters) {
this.message = message;
this.parameters = parameters;
}
public String getMessage() {
return message;
}
public Object[] getParameters() {
return parameters;
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RegexPatternsPasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordRegexPatternMessage";
private KeycloakContext context;
public RegexPatternsPasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
Pattern pattern = context.getRealm().getPasswordPolicy().getPolicyConfig(RegexPatternsPasswordPolicyProviderFactory.ID);
Matcher matcher = pattern.matcher(password);
if (!matcher.matches()) {
return new PolicyError(ERROR_MESSAGE, pattern.pattern());
}
return null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
if (value == null) {
throw new IllegalArgumentException("Config required");
}
return Pattern.compile(value);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RegexPatternsPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
static final String ID = "regexPattern";
@Override
public String getId() {
return ID;
}
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new RegexPatternsPasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getDisplayName() {
return "Regular Expression";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.STRING_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return "";
}
@Override
public boolean isMultiplSupported() {
return true;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class SpecialCharsPasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordMinSpecialCharsMessage";
private KeycloakContext context;
public SpecialCharsPasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
int min = context.getRealm().getPasswordPolicy().getPolicyConfig(SpecialCharsPasswordPolicyProviderFactory.ID);
int count = 0;
for (char c : password.toCharArray()) {
if (!Character.isLetterOrDigit(c)) {
count++;
}
}
return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class SpecialCharsPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
public static final String ID = "specialChars";
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new SpecialCharsPasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getDisplayName() {
return "Special Characters";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return "1";
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public String getId() {
return ID;
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UpperCasePasswordPolicyProvider implements PasswordPolicyProvider {
private static final String ERROR_MESSAGE = "invalidPasswordMinUpperCaseCharsMessage";
private KeycloakContext context;
public UpperCasePasswordPolicyProvider(KeycloakContext context) {
this.context = context;
}
@Override
public PolicyError validate(String username, String password) {
int min = context.getRealm().getPasswordPolicy().getPolicyConfig(UpperCasePasswordPolicyProviderFactory.ID);
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) {
count++;
}
}
return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
}
@Override
public PolicyError validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.policy;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UpperCasePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
public static final String ID = "upperCase";
@Override
public PasswordPolicyProvider create(KeycloakSession session) {
return new UpperCasePasswordPolicyProvider(session.getContext());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getDisplayName() {
return "Uppercase Characters";
}
@Override
public String getConfigType() {
return PasswordPolicyProvider.INT_CONFIG_TYPE;
}
@Override
public String getDefaultConfigValue() {
return "1";
}
@Override
public boolean isMultiplSupported() {
return false;
}
@Override
public String getId() {
return ID;
}
}

View file

@ -36,6 +36,17 @@ public class ProviderConfigProperty {
protected String type; protected String type;
protected Object defaultValue; protected Object defaultValue;
public ProviderConfigProperty() {
}
public ProviderConfigProperty(String name, String label, String helpText, String type, Object defaultValue) {
this.name = name;
this.label = label;
this.helpText = helpText;
this.type = type;
this.defaultValue = defaultValue;
}
public String getName() { public String getName() {
return name; return name;
} }

View file

@ -0,0 +1,18 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# 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.
#
org.keycloak.policy.DefaultPasswordPolicyManagerProviderFactory

View file

@ -0,0 +1,28 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# 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.
#
org.keycloak.policy.DigitsPasswordPolicyProviderFactory
org.keycloak.policy.ForceExpiredPasswordPolicyProviderFactory
org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory
org.keycloak.policy.HashIterationsPasswordPolicyProviderFactory
org.keycloak.policy.HistoryPasswordPolicyProviderFactory
org.keycloak.policy.LengthPasswordPolicyProviderFactory
org.keycloak.policy.LowerCasePasswordPolicyProviderFactory
org.keycloak.policy.NotUsernamePasswordPolicyProviderFactory
org.keycloak.policy.RegexPatternsPasswordPolicyProviderFactory
org.keycloak.policy.SpecialCharsPasswordPolicyProviderFactory
org.keycloak.policy.UpperCasePasswordPolicyProviderFactory

View file

@ -60,4 +60,5 @@ org.keycloak.authorization.store.StoreFactorySpi
org.keycloak.authorization.AuthorizationSpi org.keycloak.authorization.AuthorizationSpi
org.keycloak.models.cache.authorization.CachedStoreFactorySpi org.keycloak.models.cache.authorization.CachedStoreFactorySpi
org.keycloak.protocol.oidc.TokenIntrospectionSpi org.keycloak.protocol.oidc.TokenIntrospectionSpi
org.keycloak.policy.PasswordPolicySpi
org.keycloak.policy.PasswordPolicyManagerSpi

View file

@ -1,168 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.models;
import org.junit.Assert;
import org.junit.Test;
import java.util.regex.PatternSyntaxException;
import static org.junit.Assert.fail;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordPolicyTest {
@Test
public void testLength() {
PasswordPolicy policy = new PasswordPolicy("length");
Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate(null, "jdoe", "1234567").getMessage());
Assert.assertArrayEquals(new Object[]{8}, policy.validate(null, "jdoe", "1234567").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "12345678"));
policy = new PasswordPolicy("length(4)");
Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate(null, "jdoe", "123").getMessage());
Assert.assertArrayEquals(new Object[]{4}, policy.validate(null, "jdoe", "123").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "1234"));
}
@Test
public void testDigits() {
PasswordPolicy policy = new PasswordPolicy("digits");
Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate(null, "jdoe", "abcd").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "abcd").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "abcd1"));
policy = new PasswordPolicy("digits(2)");
Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate(null, "jdoe", "abcd1").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "abcd1").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "abcd12"));
}
@Test
public void testLowerCase() {
PasswordPolicy policy = new PasswordPolicy("lowerCase");
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate(null, "jdoe", "ABCD1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "ABCD1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "ABcD1234"));
policy = new PasswordPolicy("lowerCase(2)");
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate(null, "jdoe", "ABcD1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "ABcD1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "aBcD1234"));
}
@Test
public void testUpperCase() {
PasswordPolicy policy = new PasswordPolicy("upperCase");
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate(null, "jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "abcd1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "abCd1234"));
policy = new PasswordPolicy("upperCase(2)");
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate(null, "jdoe", "abCd1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "abCd1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "AbCd1234"));
}
@Test
public void testSpecialChars() {
PasswordPolicy policy = new PasswordPolicy("specialChars");
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate(null, "jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "abcd1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "ab&d1234"));
policy = new PasswordPolicy("specialChars(2)");
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate(null, "jdoe", "ab&d1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "ab&d1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "ab&d-234"));
}
@Test
public void testNotUsername() {
PasswordPolicy policy = new PasswordPolicy("notUsername");
Assert.assertEquals("invalidPasswordNotUsernameMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
Assert.assertNull(policy.validate(null, "jdoe", "ab&d1234"));
}
@Test
public void testInvalidPolicyName() {
try {
PasswordPolicy policy = new PasswordPolicy("noSuchPolicy");
Assert.fail("Expected exception");
} catch (IllegalArgumentException e) {
}
}
@Test
public void testRegexPatterns() {
PasswordPolicy policy = null;
try {
policy = new PasswordPolicy("regexPattern");
fail("Expected NullPointerException: Regex Pattern cannot be null.");
} catch (NullPointerException e) {
// Expected NPE as regex pattern is null.
}
try {
policy = new PasswordPolicy("regexPattern(*)");
fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
} catch (PatternSyntaxException e) {
// Expected PSE as regex pattern(or any of its token) is not quantifiable.
}
try {
policy = new PasswordPolicy("regexPattern(*,**)");
fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
} catch (PatternSyntaxException e) {
// Expected PSE as regex pattern(or any of its token) is not quantifiable.
}
//Fails to match one of the regex pattern
policy = new PasswordPolicy("regexPattern(jdoe) and regexPattern(j*d)");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
////Fails to match all of the regex patterns
policy = new PasswordPolicy("regexPattern(j*p) and regexPattern(j*d) and regexPattern(adoe)");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
policy = new PasswordPolicy("regexPattern([a-z][a-z][a-z][a-z][0-9])");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
policy = new PasswordPolicy("regexPattern(jdoe)");
Assert.assertNull(policy.validate(null, "jdoe", "jdoe"));
policy = new PasswordPolicy("regexPattern([a-z][a-z][a-z][a-z][0-9])");
Assert.assertNull(policy.validate(null, "jdoe", "jdoe0"));
}
@Test
public void testComplex() {
PasswordPolicy policy = new PasswordPolicy("length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2) and notUsername()");
Assert.assertNotNull(policy.validate(null, "jdoe", "12aaBB&"));
Assert.assertNotNull(policy.validate(null, "jdoe", "aaaaBB&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12AABB&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12aabb&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12aaBBcc"));
Assert.assertNotNull(policy.validate(null, "12aaBB&-", "12aaBB&-"));
Assert.assertNull(policy.validate(null, "jdoe", "12aaBB&-"));
}
}

View file

@ -33,6 +33,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
@ -70,7 +72,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM)); errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM));
} }
if (formData.getFirst(RegistrationPage.FIELD_PASSWORD) != null) { if (formData.getFirst(RegistrationPage.FIELD_PASSWORD) != null) {
PasswordPolicy.Error err = context.getRealm().getPasswordPolicy().validate(context.getSession(), context.getRealm().isRegistrationEmailAsUsername() ? formData.getFirst(RegistrationPage.FIELD_EMAIL) : formData.getFirst(RegistrationPage.FIELD_USERNAME), formData.getFirst(RegistrationPage.FIELD_PASSWORD)); PolicyError err = context.getSession().getProvider(PasswordPolicyManagerProvider.class).validate(context.getRealm().isRegistrationEmailAsUsername() ? formData.getFirst(RegistrationPage.FIELD_EMAIL) : formData.getFirst(RegistrationPage.FIELD_USERNAME), formData.getFirst(RegistrationPage.FIELD_PASSWORD));
if (err != null) if (err != null)
errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD, err.getMessage(), err.getParameters())); errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD, err.getMessage(), err.getParameters()));
} }

View file

@ -84,6 +84,8 @@ public class ApplianceBootstrap {
public void createMasterRealmUser(String username, String password) { public void createMasterRealmUser(String username, String password) {
RealmModel realm = session.realms().getRealm(Config.getAdminRealm()); RealmModel realm = session.realms().getRealm(Config.getAdminRealm());
session.getContext().setRealm(realm);
if (session.users().getUsersCount(realm) > 0) { if (session.users().getUsersCount(realm) > 0) {
throw new IllegalStateException("Can't create initial user as users already exists"); throw new IllegalStateException("Can't create initial user as users already exists");
} }

View file

@ -220,7 +220,7 @@ public class RealmManager implements RealmImporter {
realm.setEventsListeners(Collections.singleton("jboss-logging")); realm.setEventsListeners(Collections.singleton("jboss-logging"));
realm.setPasswordPolicy(new PasswordPolicy("hashIterations(20000)")); realm.setPasswordPolicy(PasswordPolicy.parse(session, "hashIterations(20000)"));
} }
public boolean removeRealm(RealmModel realm) { public boolean removeRealm(RealmModel realm) {

View file

@ -280,7 +280,7 @@ public class RealmAdminResource {
} }
} }
RepresentationToModel.updateRealm(rep, realm); RepresentationToModel.updateRealm(rep, realm, session);
// Refresh periodic sync tasks for configured federationProviders // Refresh periodic sync tasks for configured federationProviders
List<UserFederationProviderModel> federationProviders = realm.getUserFederationProviders(); List<UserFederationProviderModel> federationProviders = realm.getUserFederationProviders();

View file

@ -35,6 +35,11 @@ import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.policy.PasswordPolicyProvider;
import org.keycloak.policy.PasswordPolicyProviderFactory;
import org.keycloak.provider.*;
import org.keycloak.representations.idm.PasswordPolicyTypeRepresentation;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider; import org.keycloak.theme.ThemeProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -44,10 +49,6 @@ import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
@ -88,6 +89,7 @@ public class ServerInfoAdminResource {
setProtocolMapperTypes(info); setProtocolMapperTypes(info);
setBuiltinProtocolMappers(info); setBuiltinProtocolMappers(info);
setClientInstallations(info); setClientInstallations(info);
setPasswordPolicies(info);
info.setEnums(ENUMS); info.setEnums(ENUMS);
return info; return info;
} }
@ -248,6 +250,20 @@ public class ServerInfoAdminResource {
} }
} }
private void setPasswordPolicies(ServerInfoRepresentation info) {
info.setPasswordPolicies(new LinkedList<>());
for (ProviderFactory f : session.getKeycloakSessionFactory().getProviderFactories(PasswordPolicyProvider.class)) {
PasswordPolicyProviderFactory factory = (PasswordPolicyProviderFactory) f;
PasswordPolicyTypeRepresentation rep = new PasswordPolicyTypeRepresentation();
rep.setId(factory.getId());
rep.setDisplayName(factory.getDisplayName());
rep.setConfigType(factory.getConfigType());
rep.setDefaultValue(factory.getDefaultConfigValue());
rep.setMultipleSupported(factory.isMultiplSupported());
info.getPasswordPolicies().add(rep);
}
}
private static Map<String, List<String>> createEnumsMap(Class... enums) { private static Map<String, List<String>> createEnumsMap(Class... enums) {
Map<String, List<String>> m = new HashMap<>(); Map<String, List<String>> m = new HashMap<>();
for (Class e : enums) { for (Class e : enums) {

View file

@ -22,6 +22,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
@ -73,7 +75,7 @@ public class Validation {
} }
if (formData.getFirst(FIELD_PASSWORD) != null) { if (formData.getFirst(FIELD_PASSWORD) != null) {
PasswordPolicy.Error err = policy.validate(session, realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD)); PolicyError err = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm.isRegistrationEmailAsUsername() ? formData.getFirst(FIELD_EMAIL) : formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
if (err != null) if (err != null)
errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters())); errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters()));
} }

View file

@ -351,8 +351,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
public void grantAccessTokenExpiredPassword() throws Exception { public void grantAccessTokenExpiredPassword() throws Exception {
RealmResource realmResource = adminClient.realm("test"); RealmResource realmResource = adminClient.realm("test");
RealmManager.realm(realmResource).passwordPolicy( RealmManager.realm(realmResource).passwordPolicy("forceExpiredPasswordChange(1)");
new PasswordPolicy("forceExpiredPasswordChange(1)").toString());
try { try {
setTimeOffset(60 * 60 * 48); setTimeOffset(60 * 60 * 48);
@ -376,7 +375,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
.user((String) null) .user((String) null)
.assertEvent(); .assertEvent();
} finally { } finally {
RealmManager.realm(realmResource).passwordPolicy(new PasswordPolicy("").toString()); RealmManager.realm(realmResource).passwordPolicy("");
UserManager.realm(realmResource).username("test-user@localhost") UserManager.realm(realmResource).username("test-user@localhost")
.removeRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString()); .removeRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString());
} }

View file

@ -164,11 +164,11 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim"))); Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim")));
List<UserCredentialValueModel> creds = user.getCredentialsDirectly(); List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 20000); Assert.assertEquals(creds.get(0).getHashIterations(), 20000);
realmModel.setPasswordPolicy(new PasswordPolicy("hashIterations(200)")); realmModel.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "hashIterations(200)"));
Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim"))); Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim")));
creds = user.getCredentialsDirectly(); creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 200); Assert.assertEquals(creds.get(0).getHashIterations(), 200);
realmModel.setPasswordPolicy(new PasswordPolicy("hashIterations(1)")); realmModel.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "hashIterations(1)"));
} }
@Test @Test

View file

@ -42,7 +42,7 @@ public class ModelTest extends AbstractModelTest {
realm.setSslRequired(SslRequired.EXTERNAL); realm.setSslRequired(SslRequired.EXTERNAL);
realm.setVerifyEmail(true); realm.setVerifyEmail(true);
realm.setAccessTokenLifespan(1000); realm.setAccessTokenLifespan(1000);
realm.setPasswordPolicy(new PasswordPolicy("length")); realm.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "length"));
realm.setAccessCodeLifespan(1001); realm.setAccessCodeLifespan(1001);
realm.setAccessCodeLifespanUserAction(1002); realm.setAccessCodeLifespanUserAction(1002);
KeycloakModelUtils.generateRealmKeys(realm); KeycloakModelUtils.generateRealmKeys(realm);

View file

@ -0,0 +1,185 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.testsuite.model;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import java.util.regex.PatternSyntaxException;
import static org.junit.Assert.fail;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordPolicyTest extends AbstractModelTest {
private RealmModel realmModel;
private PasswordPolicyManagerProvider policyManager;
@Before
public void before() throws Exception {
super.before();
realmModel = realmManager.createRealm("JUGGLER");
session.getContext().setRealm(realmModel);
policyManager = session.getProvider(PasswordPolicyManagerProvider.class);
}
@Test
public void testLength() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "length"));
Assert.assertEquals("invalidPasswordMinLengthMessage", policyManager.validate("jdoe", "1234567").getMessage());
Assert.assertArrayEquals(new Object[]{8}, policyManager.validate("jdoe", "1234567").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "12345678"));
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "length(4)"));
Assert.assertEquals("invalidPasswordMinLengthMessage", policyManager.validate("jdoe", "123").getMessage());
Assert.assertArrayEquals(new Object[]{4}, policyManager.validate("jdoe", "123").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "1234"));
}
@Test
public void testDigits() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "digits"));
Assert.assertEquals("invalidPasswordMinDigitsMessage", policyManager.validate("jdoe", "abcd").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "abcd").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "abcd1"));
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "digits(2)"));
Assert.assertEquals("invalidPasswordMinDigitsMessage", policyManager.validate("jdoe", "abcd1").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "abcd1").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "abcd12"));
}
@Test
public void testLowerCase() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "lowerCase"));
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policyManager.validate("jdoe", "ABCD1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "ABCD1234").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "ABcD1234"));
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "lowerCase(2)"));
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policyManager.validate("jdoe", "ABcD1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "ABcD1234").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "aBcD1234"));
}
@Test
public void testUpperCase() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "upperCase"));
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policyManager.validate("jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "abcd1234").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "abCd1234"));
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "upperCase(2)"));
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policyManager.validate("jdoe", "abCd1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "abCd1234").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "AbCd1234"));
}
@Test
public void testSpecialChars() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "specialChars"));
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policyManager.validate("jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "abcd1234").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "ab&d1234"));
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "specialChars(2)"));
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policyManager.validate("jdoe", "ab&d1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "ab&d1234").getParameters());
Assert.assertNull(policyManager.validate("jdoe", "ab&d-234"));
}
@Test
public void testNotUsername() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "notUsername"));
Assert.assertEquals("invalidPasswordNotUsernameMessage", policyManager.validate("jdoe", "jdoe").getMessage());
Assert.assertNull(policyManager.validate("jdoe", "ab&d1234"));
}
@Test
public void testInvalidPolicyName() {
try {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "noSuchPolicy"));
Assert.fail("Expected exception");
} catch (IllegalArgumentException e) {
}
}
@Test
public void testRegexPatterns() {
PasswordPolicy policy = null;
try {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern"));
fail("Expected NullPointerException: Regex Pattern cannot be null.");
} catch (IllegalArgumentException e) {
// Expected NPE as regex pattern is null.
}
try {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(*)"));
fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
} catch (PatternSyntaxException e) {
// Expected PSE as regex pattern(or any of its token) is not quantifiable.
}
try {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(*,**)"));
fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
} catch (PatternSyntaxException e) {
// Expected PSE as regex pattern(or any of its token) is not quantifiable.
}
//Fails to match one of the regex pattern
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(jdoe) and regexPattern(j*d)"));
Assert.assertEquals("invalidPasswordRegexPatternMessage", policyManager.validate("jdoe", "jdoe").getMessage());
////Fails to match all of the regex patterns
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(j*p) and regexPattern(j*d) and regexPattern(adoe)"));
Assert.assertEquals("invalidPasswordRegexPatternMessage", policyManager.validate("jdoe", "jdoe").getMessage());
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern([a-z][a-z][a-z][a-z][0-9])"));
Assert.assertEquals("invalidPasswordRegexPatternMessage", policyManager.validate("jdoe", "jdoe").getMessage());
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(jdoe)"));
Assert.assertNull(policyManager.validate("jdoe", "jdoe"));
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern([a-z][a-z][a-z][a-z][0-9])"));
Assert.assertNull(policyManager.validate("jdoe", "jdoe0"));
}
@Test
public void testComplex() {
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2) and notUsername()"));
Assert.assertNotNull(policyManager.validate("jdoe", "12aaBB&"));
Assert.assertNotNull(policyManager.validate("jdoe", "aaaaBB&-"));
Assert.assertNotNull(policyManager.validate("jdoe", "12AABB&-"));
Assert.assertNotNull(policyManager.validate("jdoe", "12aabb&-"));
Assert.assertNotNull(policyManager.validate("jdoe", "12aaBBcc"));
Assert.assertNotNull(policyManager.validate("12aaBB&-", "12aaBB&-"));
Assert.assertNull(policyManager.validate("jdoe", "12aaBB&-"));
}
}

View file

@ -1639,6 +1639,9 @@ module.config([ '$routeProvider', function($routeProvider) {
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
} }
}, },
controller : 'RealmPasswordPolicyCtrl' controller : 'RealmPasswordPolicyCtrl'

View file

@ -429,68 +429,84 @@ module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache,
}); });
module.controller('RealmPasswordPolicyCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) { module.controller('RealmPasswordPolicyCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, serverInfo) {
console.log('RealmPasswordPolicyCtrl'); var parse = function(policyString) {
var policies = [];
$scope.realm = realm; if (!policyString || policyString.length == 0){
return policies;
var oldCopy = angular.copy($scope.realm);
$scope.allPolicies = PasswordPolicy.allPolicies;
$scope.policyMessages = PasswordPolicy.policyMessages;
$scope.policy = PasswordPolicy.parse(realm.passwordPolicy);
var oldPolicy = angular.copy($scope.policy);
$scope.addPolicy = function(policy){
if (!$scope.policy) {
$scope.policy = [];
} }
if (policy.name === 'regexPattern') {
for (var i in $scope.allPolicies) { var policyArray = policyString.split(" and ");
var p = $scope.allPolicies[i];
if (p.name === 'regexPattern') { for (var i = 0; i < policyArray.length; i ++){
$scope.allPolicies[i] = { name: 'regexPattern', value: '' }; var policyToken = policyArray[i];
var id;
var value;
if (policyToken.indexOf('(') == -1) {
id = policyToken.trim();
} else {
id = policyToken.substring(0, policyToken.indexOf('('));
value = policyToken.substring(policyToken.indexOf('(') + 1, policyToken.indexOf(')')).trim();
}
for (var j = 0; j < serverInfo.passwordPolicies.length; j++) {
if (serverInfo.passwordPolicies[j].id == id) {
var p = serverInfo.passwordPolicies[j];
p.value = value && value || p.defaultValue;
policies.push(p);
} }
} }
} }
return policies;
};
var toString = function(policies) {
if (!policies || policies.length == 0) {
return "";
}
var policyString = "";
for (var i = 0; i < policies.length; i++) {
policyString += policies[i].id;
if (policies[i].value && policies[i].value != policies[i].defaultValue) {
policyString += '(' + policies[i].value + ')';
}
policyString += " and ";
}
policyString = policyString.substring(0, policyString.length - 5);
return policyString;
}
$scope.realm = realm;
$scope.serverInfo = serverInfo;
$scope.changed = false; $scope.policy = parse(realm.passwordPolicy);
$scope.addPolicy = function(policy){
policy.value = policy.defaultValue;
if (!$scope.policy) {
$scope.policy = [];
}
$scope.policy.push(policy); $scope.policy.push(policy);
$scope.changed = true;
} }
$scope.removePolicy = function(index){ $scope.removePolicy = function(index){
$scope.policy.splice(index, 1); $scope.policy.splice(index, 1);
$scope.changed = true;
} }
$scope.changed = false;
$scope.$watch('realm', function() {
if (!angular.equals($scope.realm, oldCopy)) {
$scope.changed = true;
}
}, true);
$scope.$watch('policy', function(oldVal, newVal) {
if (!angular.equals($scope.policy, oldPolicy)) {
$scope.realm.passwordPolicy = PasswordPolicy.toString($scope.policy);
$scope.changed = true;
}
}, true);
$scope.save = function() { $scope.save = function() {
$scope.changed = false; $scope.changed = false;
$scope.realm.passwordPolicy = toString($scope.policy);
console.debug($scope.realm.passwordPolicy);
Realm.update($scope.realm, function () { Realm.update($scope.realm, function () {
$location.url("/realms/" + realm.realm + "/authentication/password-policy"); $location.url("/realms/" + realm.realm + "/authentication/password-policy");
Notifications.success("Your changes have been saved to the realm."); Notifications.success("Your changes have been saved to the realm.");
oldCopy = angular.copy($scope.realm);
oldPolicy = angular.copy($scope.policy);
}); });
}; };
$scope.reset = function() { $scope.reset = function() {
$scope.realm = angular.copy(oldCopy); $route.reload();
$scope.policy = angular.copy(oldPolicy);
$scope.changed = false;
}; };
}); });

View file

@ -1238,90 +1238,6 @@ module.factory('TimeUnit2', function() {
return t; return t;
}); });
module.factory('PasswordPolicy', function() {
var p = {};
p.policyMessages = {
hashAlgorithm: "Default hashing algorithm. Default is 'pbkdf2'.",
hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.",
length: "Minimal password length (integer type). Default value is 8.",
digits: "Minimal number (integer type) of digits in password. Default value is 1.",
lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.",
specialChars: "Minimal number (integer type) of special characters in password. Default value is 1.",
notUsername: "Block passwords that are equal to the username",
regexPattern: "Block passwords that do not match the regex pattern (string type).",
passwordHistory: "Block passwords that are equal to previous passwords. Default value is 3.",
forceExpiredPasswordChange: "Force password change when password credential is expired. Default value is 365 days."
}
p.allPolicies = [
{ name: 'hashAlgorithm', value: 'pbkdf2' },
{ name: 'hashIterations', value: 1 },
{ name: 'length', value: 8 },
{ name: 'digits', value: 1 },
{ name: 'lowerCase', value: 1 },
{ name: 'upperCase', value: 1 },
{ name: 'specialChars', value: 1 },
{ name: 'notUsername', value: 1 },
{ name: 'regexPattern', value: ''},
{ name: 'passwordHistory', value: 3 },
{ name: 'forceExpiredPasswordChange', value: 365 }
];
p.parse = function(policyString) {
var policies = [];
var re, policyEntry;
if (!policyString || policyString.length == 0){
return policies;
}
var policyArray = policyString.split(" and ");
for (var i = 0; i < policyArray.length; i ++){
var policyToken = policyArray[i];
if(policyToken.indexOf('hashAlgorithm') === 0 || policyToken.indexOf('regexPattern') === 0) {
re = /(\w+)\((.*)\)/;
policyEntry = re.exec(policyToken);
if (null !== policyEntry) {
policies.push({ name: policyEntry[1], value: policyEntry[2] });
}
} else {
re = /(\w+)\(*(\d*)\)*/;
policyEntry = re.exec(policyToken);
if (null !== policyEntry) {
policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
}
}
}
return policies;
};
p.toString = function(policies) {
if (!policies || policies.length == 0) {
return "";
}
var policyString = "";
for (var i = 0; i < policies.length; i++) {
policyString += policies[i].name;
if ( policies[i].value ){
policyString += '(' + policies[i].value + ')';
}
policyString += " and ";
}
policyString = policyString.substring(0, policyString.length - 5);
return policyString;
};
return p;
});
module.filter('removeSelectedPolicies', function() { module.filter('removeSelectedPolicies', function() {
return function(policies, selectedPolicies) { return function(policies, selectedPolicies) {
var result = []; var result = [];
@ -1329,7 +1245,7 @@ module.filter('removeSelectedPolicies', function() {
var policy = policies[i]; var policy = policies[i];
var policyAvailable = true; var policyAvailable = true;
for(var j in selectedPolicies) { for(var j in selectedPolicies) {
if(policy.name === selectedPolicies[j].name && policy.name !== 'regexPattern') { if(policy.id === selectedPolicies[j].id && !policy.multipleSupported) {
policyAvailable = false; policyAvailable = false;
} }
} }

View file

@ -7,12 +7,12 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<caption class="hidden">{{:: 'table-of-password-policies' | translate}}</caption> <caption class="hidden">{{:: 'table-of-password-policies' | translate}}</caption>
<thead> <thead>
<tr ng-show="(allPolicies|removeSelectedPolicies:policy).length > 0"> <tr ng-show="(serverInfo.passwordPolicies|removeSelectedPolicies:policy).length > 0">
<th colspan="5" class="kc-table-actions"> <th colspan="5" class="kc-table-actions">
<div class="pull-right"> <div class="pull-right">
<div> <div>
<select class="form-control" ng-model="selectedPolicy" <select class="form-control" ng-model="selectedPolicy"
ng-options="(p.name|capitalize) for p in (allPolicies|removeSelectedPolicies:policy)" ng-options="policy as policy.displayName for policy in (serverInfo.passwordPolicies|removeSelectedPolicies:policy) track by policy.id"
data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null"> data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
<option value="" disabled selected>{{:: 'add-policy.placeholder' | translate}}</option> <option value="" disabled selected>{{:: 'add-policy.placeholder' | translate}}</option>
</select> </select>
@ -28,10 +28,9 @@
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="p in policy"> <tr ng-repeat="p in policy">
<td>{{p.name|capitalize}}</td> <td>{{p.displayName}}</td>
<td> <td>
<input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' " <input type="text" class="form-control" ng-model="p.value" ng-show="p.configType" data-ng-required="!p.configType && !p.defaultValue">
placeholder="{{:: 'no-value-assigned.placeholder' | translate}}" min="1" required>
</td> </td>
<td class="kc-action-cell" ng-click="removePolicy($index)">{{:: 'delete' | translate}}</td> <td class="kc-action-cell" ng-click="removePolicy($index)">{{:: 'delete' | translate}}</td>
</tr> </tr>