feature: password age in days policy
Closes #30210 Signed-off-by: Maciej Mierzwa <dev.maciej.mierzwa@gmail.com>
This commit is contained in:
parent
c8d64f2891
commit
97e89e2071
7 changed files with 460 additions and 5 deletions
|
@ -102,6 +102,11 @@ The number of days the password is valid. When the number of days has expired, t
|
||||||
|
|
||||||
Password cannot be already used by the user. {project_name} stores a history of used passwords. The number of old passwords stored is configurable in {project_name}.
|
Password cannot be already used by the user. {project_name} stores a history of used passwords. The number of old passwords stored is configurable in {project_name}.
|
||||||
|
|
||||||
|
===== Not recently used (In Days)
|
||||||
|
|
||||||
|
Password cannot be already used by the user. {project_name} stores a history of used passwords. If the new password creation date is older then the date defined in policy, and is not currently in use, the password change will be allowed.
|
||||||
|
In case of both password history policies used, more restrictive policy is used.
|
||||||
|
|
||||||
===== Password blacklist
|
===== Password blacklist
|
||||||
Password must not be in a blacklist file.
|
Password must not be in a blacklist file.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.common.util.Time;
|
||||||
|
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:dev.maciej.mierzwa@gmail.com">Maciej Mierzwa</a>
|
||||||
|
*/
|
||||||
|
public class AgePasswordPolicyProvider implements PasswordPolicyProvider {
|
||||||
|
private static final String ERROR_MESSAGE = "invalidPasswordGenericMessage";
|
||||||
|
public static final Logger logger = Logger.getLogger(AgePasswordPolicyProvider.class);
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public AgePasswordPolicyProvider(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyError validate(String user, String password) {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
return validate(realm, session.users().getUserByUsername(realm, user), password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PolicyError validate(RealmModel realm, UserModel user, String password) {
|
||||||
|
PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
|
||||||
|
int passwordAgePolicyValue = policy.getPolicyConfig(PasswordPolicy.PASSWORD_AGE);
|
||||||
|
|
||||||
|
if (passwordAgePolicyValue != -1) {
|
||||||
|
//current password check
|
||||||
|
if (user.credentialManager().getStoredCredentialsByTypeStream(PasswordCredentialModel.TYPE)
|
||||||
|
.map(PasswordCredentialModel::createFromCredentialModel)
|
||||||
|
.anyMatch(passwordCredential -> {
|
||||||
|
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class,
|
||||||
|
passwordCredential.getPasswordCredentialData().getAlgorithm());
|
||||||
|
return hash != null && hash.verify(password, passwordCredential);
|
||||||
|
})) {
|
||||||
|
return new PolicyError(ERROR_MESSAGE, passwordAgePolicyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
final long passwordMaxAgeMillis = Time.currentTimeMillis() - Duration.ofDays(passwordAgePolicyValue).toMillis();
|
||||||
|
if (passwordAgePolicyValue > 0) {
|
||||||
|
if (user.credentialManager().getStoredCredentialsByTypeStream(PasswordCredentialModel.PASSWORD_HISTORY)
|
||||||
|
.filter(credentialModel -> credentialModel.getCreatedDate() > passwordMaxAgeMillis)
|
||||||
|
.map(PasswordCredentialModel::createFromCredentialModel)
|
||||||
|
.anyMatch(passwordCredential -> {
|
||||||
|
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class,
|
||||||
|
passwordCredential.getPasswordCredentialData().getAlgorithm());
|
||||||
|
return hash.verify(password, passwordCredential);
|
||||||
|
})) {
|
||||||
|
return new PolicyError(ERROR_MESSAGE, passwordAgePolicyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object parseConfig(String value) {
|
||||||
|
return parseInteger(value, AgePasswordPolicyProviderFactory.DEFAULT_AGE_DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.PasswordPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:dev.maciej.mierzwa@gmail.com">Maciej Mierzwa</a>
|
||||||
|
*/
|
||||||
|
public class AgePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
|
||||||
|
public static final Integer DEFAULT_AGE_DAYS = 30;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PasswordPolicy.PASSWORD_AGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName() {
|
||||||
|
return "Not Recently Used (In Days)";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getConfigType() {
|
||||||
|
return PasswordPolicyProvider.INT_CONFIG_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDefaultConfigValue() {
|
||||||
|
return String.valueOf(DEFAULT_AGE_DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMultiplSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordPolicyProvider create(KeycloakSession session) {
|
||||||
|
return new AgePasswordPolicyProvider(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,3 +32,4 @@ org.keycloak.policy.BlacklistPasswordPolicyProviderFactory
|
||||||
org.keycloak.policy.NotEmailPasswordPolicyProviderFactory
|
org.keycloak.policy.NotEmailPasswordPolicyProviderFactory
|
||||||
org.keycloak.policy.RecoveryCodesWarningThresholdPasswordPolicyProviderFactory
|
org.keycloak.policy.RecoveryCodesWarningThresholdPasswordPolicyProviderFactory
|
||||||
org.keycloak.policy.MaxAuthAgePasswordPolicyProviderFactory
|
org.keycloak.policy.MaxAuthAgePasswordPolicyProviderFactory
|
||||||
|
org.keycloak.policy.AgePasswordPolicyProviderFactory
|
|
@ -45,6 +45,8 @@ public class PasswordPolicy implements Serializable {
|
||||||
|
|
||||||
public static final String MAX_AUTH_AGE_ID = "maxAuthAge";
|
public static final String MAX_AUTH_AGE_ID = "maxAuthAge";
|
||||||
|
|
||||||
|
public static final String PASSWORD_AGE = "passwordAge";
|
||||||
|
|
||||||
private Map<String, Object> policyConfig;
|
private Map<String, Object> policyConfig;
|
||||||
private Builder builder;
|
private Builder builder;
|
||||||
|
|
||||||
|
@ -97,6 +99,14 @@ public class PasswordPolicy implements Serializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getPasswordAgeInDays() {
|
||||||
|
if (policyConfig.containsKey(PASSWORD_AGE)) {
|
||||||
|
return getPolicyConfig(PASSWORD_AGE);
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int getDaysToExpirePassword() {
|
public int getDaysToExpirePassword() {
|
||||||
if (policyConfig.containsKey(FORCE_EXPIRED_ID)) {
|
if (policyConfig.containsKey(FORCE_EXPIRED_ID)) {
|
||||||
return getPolicyConfig(FORCE_EXPIRED_ID);
|
return getPolicyConfig(FORCE_EXPIRED_ID);
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
import org.keycloak.policy.PasswordPolicyManagerProvider;
|
import org.keycloak.policy.PasswordPolicyManagerProvider;
|
||||||
import org.keycloak.policy.PolicyError;
|
import org.keycloak.policy.PolicyError;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -79,6 +80,7 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
|
||||||
|
|
||||||
PasswordPolicy policy = realm.getPasswordPolicy();
|
PasswordPolicy policy = realm.getPasswordPolicy();
|
||||||
int expiredPasswordsPolicyValue = policy.getExpiredPasswords();
|
int expiredPasswordsPolicyValue = policy.getExpiredPasswords();
|
||||||
|
int passwordAgeInDaysPolicy = Math.max(0, policy.getPasswordAgeInDays());
|
||||||
|
|
||||||
// 1) create new or reset existing password
|
// 1) create new or reset existing password
|
||||||
CredentialModel createdCredential;
|
CredentialModel createdCredential;
|
||||||
|
@ -94,24 +96,34 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
|
||||||
createdCredential = credentialModel;
|
createdCredential = credentialModel;
|
||||||
|
|
||||||
// 2) add a password history item based on the old password
|
// 2) add a password history item based on the old password
|
||||||
if (expiredPasswordsPolicyValue > 1) {
|
if (expiredPasswordsPolicyValue > 1 || passwordAgeInDaysPolicy > 0) {
|
||||||
oldPassword.setId(null);
|
oldPassword.setId(null);
|
||||||
oldPassword.setType(PasswordCredentialModel.PASSWORD_HISTORY);
|
oldPassword.setType(PasswordCredentialModel.PASSWORD_HISTORY);
|
||||||
user.credentialManager().createStoredCredential(oldPassword);
|
oldPassword = user.credentialManager().createStoredCredential(oldPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) remove old password history items
|
// 3) remove old password history items, if both history policies are set, more restrictive policy wins
|
||||||
final int passwordHistoryListMaxSize = Math.max(0, expiredPasswordsPolicyValue - 1);
|
final int passwordHistoryListMaxSize = Math.max(0, expiredPasswordsPolicyValue - 1);
|
||||||
|
|
||||||
|
final long passwordMaxAgeMillis = Time.currentTimeMillis() - Duration.ofDays(passwordAgeInDaysPolicy).toMillis();
|
||||||
|
|
||||||
|
CredentialModel finalOldPassword = oldPassword;
|
||||||
user.credentialManager().getStoredCredentialsByTypeStream(PasswordCredentialModel.PASSWORD_HISTORY)
|
user.credentialManager().getStoredCredentialsByTypeStream(PasswordCredentialModel.PASSWORD_HISTORY)
|
||||||
.sorted(CredentialModel.comparingByStartDateDesc())
|
.sorted(CredentialModel.comparingByStartDateDesc())
|
||||||
.skip(passwordHistoryListMaxSize)
|
.skip(passwordHistoryListMaxSize)
|
||||||
|
.filter(credentialModel1 -> !(credentialModel1.getId().equals(finalOldPassword.getId())))
|
||||||
|
.filter(credential -> passwordAgePredicate(credential, passwordMaxAgeMillis))
|
||||||
.collect(Collectors.toList())
|
.collect(Collectors.toList())
|
||||||
.forEach(p -> user.credentialManager().removeStoredCredentialById(p.getId()));
|
.forEach(p -> user.credentialManager().removeStoredCredentialById(p.getId()));
|
||||||
|
|
||||||
return createdCredential;
|
return createdCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean passwordAgePredicate(CredentialModel credential, long passwordMaxAgeMillis) {
|
||||||
|
return credential.getCreatedDate() < passwordMaxAgeMillis;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
|
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
|
||||||
return user.credentialManager().removeStoredCredentialById(credentialId);
|
return user.credentialManager().removeStoredCredentialById(credentialId);
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.policy;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.BadRequestException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractAuthTest;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;
|
||||||
|
import static org.keycloak.testsuite.admin.ApiUtil.getCreatedId;
|
||||||
|
|
||||||
|
|
||||||
|
public class PasswordAgePolicyTest extends AbstractAuthTest {
|
||||||
|
|
||||||
|
UserResource user;
|
||||||
|
|
||||||
|
private void setPasswordAgePolicy(String passwordAge) {
|
||||||
|
log.info(String.format("Setting %s", passwordAge));
|
||||||
|
RealmRepresentation testRealmRepresentation = testRealmResource().toRepresentation();
|
||||||
|
testRealmRepresentation.setPasswordPolicy(passwordAge);
|
||||||
|
testRealmResource().update(testRealmRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPasswordHistory(String passwordHistory) {
|
||||||
|
log.info(String.format("Setting %s", passwordHistory));
|
||||||
|
RealmRepresentation testRealmRepresentation = testRealmResource().toRepresentation();
|
||||||
|
testRealmRepresentation.setPasswordPolicy(passwordHistory);
|
||||||
|
testRealmResource().update(testRealmRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPasswordAgePolicyValue(String value) {
|
||||||
|
setPasswordAgePolicy(String.format("passwordAge(%s)", value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPasswordAgePolicyValue(int value) {
|
||||||
|
setPasswordAgePolicyValue(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPasswordHistoryValue(String value) {
|
||||||
|
setPasswordHistory(String.format("passwordHistory(%s)", value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPasswordHistoryValue(int value) {
|
||||||
|
setPasswordHistoryValue(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserRepresentation createUserRepresentation(String username) {
|
||||||
|
UserRepresentation userRepresentation = new UserRepresentation();
|
||||||
|
userRepresentation.setUsername(username);
|
||||||
|
userRepresentation.setEmail(String.format("%s@email.test", userRepresentation.getUsername()));
|
||||||
|
userRepresentation.setEmailVerified(true);
|
||||||
|
return userRepresentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserResource createUser(UserRepresentation user) {
|
||||||
|
String createdUserId;
|
||||||
|
try (Response response = testRealmResource().users().create(user)) {
|
||||||
|
createdUserId = getCreatedId(response);
|
||||||
|
}
|
||||||
|
return testRealmResource().users().get(createdUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetUserPassword(UserResource userResource, String newPassword) {
|
||||||
|
CredentialRepresentation newCredential = new CredentialRepresentation();
|
||||||
|
newCredential.setType(PASSWORD);
|
||||||
|
newCredential.setValue(newPassword);
|
||||||
|
newCredential.setTemporary(false);
|
||||||
|
userResource.resetPassword(newCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expectBadRequestException(Consumer<Void> f) {
|
||||||
|
try {
|
||||||
|
f.accept(null);
|
||||||
|
throw new AssertionError("An expected BadRequestException was not thrown.");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
log.info("An expected BadRequestException was caught.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static private int daysToSeconds(int days) {
|
||||||
|
return days * 24 * 60 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
user = createUser(createUserRepresentation("test_user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void after() {
|
||||||
|
user.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordHistoryRetrySamePassword() {
|
||||||
|
setPasswordAgePolicyValue(1);
|
||||||
|
//set offset to 12h ago
|
||||||
|
setTimeOffset(-12 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
//try to set again same password
|
||||||
|
setTimeOffset(0);
|
||||||
|
expectBadRequestException(f -> resetUserPassword(user, "secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordHistoryWithTwoPasswordsErrorThrown() {
|
||||||
|
setPasswordAgePolicyValue(1);
|
||||||
|
//set offset to 12h ago
|
||||||
|
setTimeOffset(-12 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
setTimeOffset(-10 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
|
||||||
|
//try to set again same password after 12h
|
||||||
|
setTimeOffset(0);
|
||||||
|
expectBadRequestException(f -> resetUserPassword(user, "secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordHistoryWithTwoPasswords() {
|
||||||
|
setPasswordAgePolicyValue(1);
|
||||||
|
//set offset to more than a day ago
|
||||||
|
setTimeOffset(-24 * 60 * 60 * 2);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
setTimeOffset(-10 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
|
||||||
|
//try to set again same password after 48h
|
||||||
|
setTimeOffset(0);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordHistoryWithMultiplePasswordsErrorThrown() {
|
||||||
|
setPasswordAgePolicyValue(30);
|
||||||
|
//set offset to 29 days, 23:45:00
|
||||||
|
setTimeOffset(-30 * 24 * 60 * 60 + 15 * 60);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
setTimeOffset(-25 * 24 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
setTimeOffset(-20 * 24 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret2");
|
||||||
|
setTimeOffset(-10 * 24 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret3");
|
||||||
|
|
||||||
|
//try to set again same password after 30 days, should throw error, 15 minutes too early
|
||||||
|
setTimeOffset(0);
|
||||||
|
expectBadRequestException(f -> resetUserPassword(user, "secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordHistoryWithMultiplePasswords() {
|
||||||
|
setPasswordAgePolicyValue(30);
|
||||||
|
//set offset to 30 days and 15 minutes
|
||||||
|
setTimeOffset(-30 * 24 * 60 * 60 - 5 * 60);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
setTimeOffset(-25 * 24 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
setTimeOffset(-20 * 24 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret2");
|
||||||
|
setTimeOffset(-10 * 24 * 60 * 60);
|
||||||
|
resetUserPassword(user, "secret3");
|
||||||
|
//try to set again same password after 30 days and 15 minutes
|
||||||
|
|
||||||
|
setTimeOffset(0);
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordAge0Days() {
|
||||||
|
setPasswordAgePolicyValue(0);
|
||||||
|
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
//can't set the same password
|
||||||
|
expectBadRequestException(f -> resetUserPassword(user, "secret"));
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordAgeSetToNegative() {
|
||||||
|
setPasswordAgePolicyValue(-1);
|
||||||
|
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
//no check is performed
|
||||||
|
setPasswordAgePolicyValue(10);
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
resetUserPassword(user, "secret2");
|
||||||
|
resetUserPassword(user, "secret3");
|
||||||
|
setPasswordAgePolicyValue(-2);
|
||||||
|
//no check is performed
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
setPasswordAgePolicyValue(-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordAgeSetToInvalid() {
|
||||||
|
expectBadRequestException(f -> setPasswordAgePolicyValue("abc"));
|
||||||
|
expectBadRequestException(f -> setPasswordAgePolicyValue("2a"));
|
||||||
|
expectBadRequestException(f -> setPasswordAgePolicyValue("asda2"));
|
||||||
|
expectBadRequestException(f -> setPasswordAgePolicyValue("-/!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBothPasswordHistoryPoliciesPasswordHistoryPolicyTakesOver() {
|
||||||
|
//1 day
|
||||||
|
setPasswordAgePolicyValue(1);
|
||||||
|
//last 3 passwords
|
||||||
|
setPasswordHistoryValue(3);
|
||||||
|
setTimeOffset(daysToSeconds(-2));
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
resetUserPassword(user, "secret2");
|
||||||
|
|
||||||
|
setTimeOffset(daysToSeconds(0));
|
||||||
|
//password history takes precedence
|
||||||
|
expectBadRequestException(f -> setPasswordAgePolicyValue("secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBothPasswordHistoryPoliciesPasswordAgePolicyTakesOver() {
|
||||||
|
//2 days
|
||||||
|
setPasswordAgePolicyValue(2);
|
||||||
|
//last 10 passwords
|
||||||
|
setPasswordHistoryValue(10);
|
||||||
|
setTimeOffset(daysToSeconds(-1));
|
||||||
|
resetUserPassword(user, "secret");
|
||||||
|
resetUserPassword(user, "secret1");
|
||||||
|
resetUserPassword(user, "secret2");
|
||||||
|
|
||||||
|
setTimeOffset(daysToSeconds(0));
|
||||||
|
//password age takes precedence
|
||||||
|
expectBadRequestException(f -> setPasswordAgePolicyValue("secret"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue