diff --git a/repo/security-impl/src/main/java/com/evolveum/midpoint/security/impl/AuthenticationEvaluatorImpl.java b/repo/security-impl/src/main/java/com/evolveum/midpoint/security/impl/AuthenticationEvaluatorImpl.java index 73e69bd16e5..6dc21379ba6 100644 --- a/repo/security-impl/src/main/java/com/evolveum/midpoint/security/impl/AuthenticationEvaluatorImpl.java +++ b/repo/security-impl/src/main/java/com/evolveum/midpoint/security/impl/AuthenticationEvaluatorImpl.java @@ -236,6 +236,19 @@ private void recordAuthenticationSuccess(MidPointPrincipal principal, Connection private void recordAuthenticationFailure(MidPointPrincipal principal, ConnectionEnvironment connEnv, PasswordType passwordType, PasswordCredentialsPolicyType passwordCredentialsPolicy) { Integer failedLogins = passwordType.getFailedLogins(); + Duration lockoutFailedAttemptsDuration = passwordCredentialsPolicy.getLockoutFailedAttemptsDuration(); + if (lockoutFailedAttemptsDuration != null) { + LoginEventType lastFailedLogin = passwordType.getLastFailedLogin(); + if (lastFailedLogin != null) { + XMLGregorianCalendar lastFailedLoginTs = lastFailedLogin.getTimestamp(); + if (lastFailedLoginTs != null) { + XMLGregorianCalendar failedLoginsExpirationTs = XmlTypeConverter.addDuration(lastFailedLoginTs, lockoutFailedAttemptsDuration); + if (clock.isPast(failedLoginsExpirationTs)) { + failedLogins = 0; + } + } + } + } if (failedLogins == null) { passwordType.setFailedLogins(1); } else { diff --git a/repo/security-impl/src/test/java/com/evolveum/midpoint/security/impl/TestAuthenticationEvaluator.java b/repo/security-impl/src/test/java/com/evolveum/midpoint/security/impl/TestAuthenticationEvaluator.java index 1245664d2fa..1510af560b7 100644 --- a/repo/security-impl/src/test/java/com/evolveum/midpoint/security/impl/TestAuthenticationEvaluator.java +++ b/repo/security-impl/src/test/java/com/evolveum/midpoint/security/impl/TestAuthenticationEvaluator.java @@ -24,15 +24,20 @@ import java.io.File; +import javax.xml.datatype.XMLGregorianCalendar; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; +import org.testng.AssertJUnit; import org.testng.annotations.Listeners; import org.testng.annotations.Test; import com.evolveum.midpoint.common.Clock; +import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.security.api.AuthenticationEvaluator; import com.evolveum.midpoint.security.api.ConnectionEnvironment; @@ -42,8 +47,11 @@ import com.evolveum.midpoint.test.util.MidPointAsserts; import com.evolveum.midpoint.test.util.MidPointTestConstants; import com.evolveum.midpoint.test.util.TestUtil; +import com.evolveum.midpoint.util.exception.ObjectNotFoundException; +import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; +import com.evolveum.midpoint.xml.ns._public.common.common_3.LoginEventType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SecurityPolicyType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SystemConfigurationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SystemObjectsType; @@ -131,16 +139,326 @@ public void test100PasswordLoginGoodPasswordJack() throws Exception { assertPrincipalJack(principal); ConnectionEnvironment connEnv = createConnectionEnvironment(); + XMLGregorianCalendar startTs = clock.currentTimeXMLGregorianCalendar(); // WHEN TestUtil.displayWhen(TEST_NAME); Authentication authentication = authenticationEvaluator.authenticateUserPassword(principal, connEnv, USER_JACK_PASSWORD); + // THEN + TestUtil.displayThen(TEST_NAME); + XMLGregorianCalendar endTs = clock.currentTimeXMLGregorianCalendar(); + assertGoodPasswordAuthentication(authentication, principal); + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 0); + assertLastSuccessfulLogin(userAfter, startTs, endTs); + } + + @Test + public void test101PasswordLoginBadPasswordJack() throws Exception { + final String TEST_NAME = "test101PasswordLoginBadPasswordJack"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + XMLGregorianCalendar startTs = clock.currentTimeXMLGregorianCalendar(); + + try { + + // WHEN + TestUtil.displayWhen(TEST_NAME); + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, "thisIsNotMyPassword"); + + AssertJUnit.fail("Unexpected success"); + + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertBadPasswordException(e, principal); + } + XMLGregorianCalendar endTs = clock.currentTimeXMLGregorianCalendar(); + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 1); + assertLastFailedLogin(userAfter, startTs, endTs); + } + + @Test + public void test102PasswordLoginNullPasswordJack() throws Exception { + final String TEST_NAME = "test102PasswordLoginNullPasswordJack"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + + try { + + // WHEN + TestUtil.displayWhen(TEST_NAME); + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, null); + + AssertJUnit.fail("Unexpected success"); + + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertDeniedException(e, principal); + } + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 1); + } + + + @Test + public void test103PasswordLoginEmptyPasswordJack() throws Exception { + final String TEST_NAME = "test103PasswordLoginEmptyPasswordJack"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + + try { + + // WHEN + TestUtil.displayWhen(TEST_NAME); + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, ""); + + AssertJUnit.fail("Unexpected success"); + + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertDeniedException(e, principal); + } + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 1); + } + + /** + * Wait for 5 minutes. The failed login count should reset after 3 minutes. Therefore bad login + * count should be one after we try to make a bad login. + */ + @Test + public void test105PasswordLoginBadPasswordJackAfterLockoutFailedAttemptsDuration() throws Exception { + final String TEST_NAME = "test105PasswordLoginBadPasswordJackAfterLockoutFailedAttemptsDuration"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + clock.overrideDuration("PT5M"); + + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + XMLGregorianCalendar startTs = clock.currentTimeXMLGregorianCalendar(); + + try { + + // WHEN + TestUtil.displayWhen(TEST_NAME); + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, "thisIsNotMyPassword"); + + AssertJUnit.fail("Unexpected success"); + + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertBadPasswordException(e, principal); + } + XMLGregorianCalendar endTs = clock.currentTimeXMLGregorianCalendar(); + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 1); + assertLastFailedLogin(userAfter, startTs, endTs); + } + + + @Test + public void test110PasswordLoginLockout() throws Exception { + final String TEST_NAME = "test110PasswordLoginLockout"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + XMLGregorianCalendar startTs = clock.currentTimeXMLGregorianCalendar(); + + // WHEN + TestUtil.displayWhen(TEST_NAME); + try { + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, "not my password either"); + + AssertJUnit.fail("Unexpected success"); + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertBadPasswordException(e, principal); + } + + PrismObject userBetween = getUser(USER_JACK_OID); + display("user after", userBetween); + assertFailedLogins(userBetween, 2); + + try { + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, "absoLUTELY NOT my PASSword"); + + AssertJUnit.fail("Unexpected success"); + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertBadPasswordException(e, principal); + } + + + XMLGregorianCalendar endTs = clock.currentTimeXMLGregorianCalendar(); + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 3); + assertLastFailedLogin(userAfter, startTs, endTs); + } + + @Test + public void test112PasswordLoginLockedoutGoodPassword() throws Exception { + final String TEST_NAME = "test112PasswordLoginLockedoutGoodPassword"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + + // WHEN + TestUtil.displayWhen(TEST_NAME); + try { + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, USER_JACK_PASSWORD); + + AssertJUnit.fail("Unexpected success"); + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + assertLockedException(e, principal); + } + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 3); + } + + @Test + public void test113PasswordLoginLockedoutBadPassword() throws Exception { + final String TEST_NAME = "test113PasswordLoginLockedoutBadPassword"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + + // WHEN + TestUtil.displayWhen(TEST_NAME); + try { + + authenticationEvaluator.authenticateUserPassword(principal, connEnv, "bad bad password!"); + + AssertJUnit.fail("Unexpected success"); + } catch (BadCredentialsException e) { + // This is expected + + // THEN + TestUtil.displayThen(TEST_NAME); + display("expected exception", e); + + // this is important. The exception should give no indication whether the password is + // good or bad + assertLockedException(e, principal); + } + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 3); + } + + + @Test + public void test118PasswordLoginLockedoutLockExpires() throws Exception { + final String TEST_NAME = "test118PasswordLoginLockedoutLockExpires"; + TestUtil.displayTestTile(TEST_NAME); + + // GIVEN + clock.overrideDuration("PT30M"); + + MidPointPrincipal principal = userProfileService.getPrincipal(USER_JACK_USERNAME); + assertPrincipalJack(principal); + + ConnectionEnvironment connEnv = createConnectionEnvironment(); + XMLGregorianCalendar startTs = clock.currentTimeXMLGregorianCalendar(); + // WHEN + TestUtil.displayWhen(TEST_NAME); + Authentication authentication = authenticationEvaluator.authenticateUserPassword(principal, connEnv, USER_JACK_PASSWORD); + + // THEN TestUtil.displayThen(TEST_NAME); + XMLGregorianCalendar endTs = clock.currentTimeXMLGregorianCalendar(); assertGoodPasswordAuthentication(authentication, principal); + + PrismObject userAfter = getUser(USER_JACK_OID); + display("user after", userAfter); + assertFailedLogins(userAfter, 0); + assertLastSuccessfulLogin(userAfter, startTs, endTs); } + private void assertGoodPasswordAuthentication(Authentication authentication, MidPointPrincipal principal) { assertNotNull("No authentication", authentication); assertTrue("authentication: not authenticated", authentication.isAuthenticated()); @@ -148,9 +466,53 @@ private void assertGoodPasswordAuthentication(Authentication authentication, Mid assertEquals("authentication: principal mismatch", principal, authentication.getPrincipal()); } + private void assertBadPasswordException(BadCredentialsException e, MidPointPrincipal principal) { + assertEquals("Wrong exception meessage (key)", "web.security.provider.invalid", e.getMessage()); + } + + private void assertDeniedException(BadCredentialsException e, MidPointPrincipal principal) { + assertEquals("Wrong exception meessage (key)", "web.security.provider.access.denied", e.getMessage()); + } + + private void assertLockedException(BadCredentialsException e, MidPointPrincipal principal) { + assertEquals("Wrong exception meessage (key)", "web.security.provider.locked", e.getMessage()); + } + private ConnectionEnvironment createConnectionEnvironment() { ConnectionEnvironment connEnv = new ConnectionEnvironment(); connEnv.setRemoteHost("remote.example.com"); return connEnv; } + + private PrismObject getUser(String oid) throws ObjectNotFoundException, SchemaException { + OperationResult result = new OperationResult("getUser"); + PrismObject user = repositoryService.getObject(UserType.class, oid, null, result); + result.computeStatus(); + TestUtil.assertSuccess(result); + return user; + } + + private void assertFailedLogins(PrismObject user, int expected) { + if (expected == 0 && user.asObjectable().getCredentials().getPassword().getFailedLogins() == null) { + return; + } + assertEquals("Wrong failed logins in "+user, (Integer)expected, user.asObjectable().getCredentials().getPassword().getFailedLogins()); + } + + private void assertLastSuccessfulLogin(PrismObject user, XMLGregorianCalendar startTs, + XMLGregorianCalendar endTs) { + LoginEventType lastSuccessfulLogin = user.asObjectable().getCredentials().getPassword().getLastSuccessfulLogin(); + assertNotNull("no last successful login in "+user, lastSuccessfulLogin); + XMLGregorianCalendar successfulLoginTs = lastSuccessfulLogin.getTimestamp(); + TestUtil.assertBetween("wrong last successful login timestamp", startTs, endTs, successfulLoginTs); + } + + private void assertLastFailedLogin(PrismObject user, XMLGregorianCalendar startTs, + XMLGregorianCalendar endTs) { + LoginEventType lastFailedLogin = user.asObjectable().getCredentials().getPassword().getLastFailedLogin(); + assertNotNull("no last failed login in "+user, lastFailedLogin); + XMLGregorianCalendar failedLoginTs = lastFailedLogin.getTimestamp(); + TestUtil.assertBetween("wrong last failed login timestamp", startTs, endTs, failedLoginTs); + } + } diff --git a/repo/security-impl/src/test/resources/security-policy.xml b/repo/security-impl/src/test/resources/security-policy.xml index 4b4c2e5c024..ad75bc51870 100644 --- a/repo/security-impl/src/test/resources/security-policy.xml +++ b/repo/security-impl/src/test/resources/security-policy.xml @@ -19,6 +19,12 @@ xmlns='http://midpoint.evolveum.com/xml/ns/public/common/common-3'> Security Policy + + P10Y + 3 + PT3M + PT15M + http://midpoint.evolveum.com/xml/ns/public/security/question-2#q001 diff --git a/repo/security-impl/src/test/resources/user-jack.xml b/repo/security-impl/src/test/resources/user-jack.xml index 9ee39274e67..248e0aaeb52 100644 --- a/repo/security-impl/src/test/resources/user-jack.xml +++ b/repo/security-impl/src/test/resources/user-jack.xml @@ -36,6 +36,7 @@ + deadmentellnotales