-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from OpenLMIS/OIS-46
OIS-46: Added login lockout mechanism
- Loading branch information
Showing
16 changed files
with
506 additions
and
8 deletions.
There are no files selected for viewing
86 changes: 86 additions & 0 deletions
86
.../openlmis/auth/repository/UnsuccessfulAuthenticationAttemptRepositoryIntegrationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/* | ||
* This program is part of the OpenLMIS logistics management information system platform software. | ||
* Copyright © 2017 VillageReach | ||
* | ||
* This program is free software: you can redistribute it and/or modify it under the terms | ||
* of the GNU Affero General Public License as published by the Free Software Foundation, either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | ||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
* See the GNU Affero General Public License for more details. You should have received a copy of | ||
* the GNU Affero General Public License along with this program. If not, see | ||
* http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. | ||
*/ | ||
|
||
package org.openlmis.auth.repository; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertNotNull; | ||
|
||
import java.util.UUID; | ||
import javax.persistence.EntityManager; | ||
import javax.persistence.PersistenceException; | ||
import org.junit.Test; | ||
import org.openlmis.auth.domain.UnsuccessfulAuthenticationAttempt; | ||
import org.openlmis.auth.domain.User; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.data.repository.CrudRepository; | ||
|
||
public class UnsuccessfulAuthenticationAttemptRepositoryIntegrationTest | ||
extends BaseCrudRepositoryIntegrationTest<UnsuccessfulAuthenticationAttempt> { | ||
|
||
@Autowired | ||
private UnsuccessfulAuthenticationAttemptRepository unsuccessfulAuthenticationAttemptRepository; | ||
|
||
@Autowired | ||
private UserRepository userRepository; | ||
|
||
@Autowired | ||
private EntityManager entityManager; | ||
|
||
@Test | ||
public void shouldFindAttemptByUserId() throws Exception { | ||
User user = userRepository.save(generateUser()); | ||
UnsuccessfulAuthenticationAttempt attempt = | ||
unsuccessfulAuthenticationAttemptRepository.save(generateInstance(user)); | ||
|
||
UnsuccessfulAuthenticationAttempt result = | ||
unsuccessfulAuthenticationAttemptRepository.findByUserId(user.getId()).get(); | ||
|
||
assertNotNull(result); | ||
assertEquals(attempt.getId(), result.getId()); | ||
} | ||
|
||
@Test(expected = PersistenceException.class) | ||
public void shouldThrowExceptionOnCreatingAttemptsWithSameUser() throws Exception { | ||
User user = userRepository.save(generateUser()); | ||
|
||
unsuccessfulAuthenticationAttemptRepository.save(generateInstance(user)); | ||
unsuccessfulAuthenticationAttemptRepository.save(generateInstance(user)); | ||
|
||
entityManager.flush(); | ||
} | ||
|
||
@Override | ||
CrudRepository<UnsuccessfulAuthenticationAttempt, UUID> getRepository() { | ||
return unsuccessfulAuthenticationAttemptRepository; | ||
} | ||
|
||
@Override | ||
UnsuccessfulAuthenticationAttempt generateInstance() throws Exception { | ||
return new UnsuccessfulAuthenticationAttempt(userRepository.save(generateUser())); | ||
} | ||
|
||
UnsuccessfulAuthenticationAttempt generateInstance(User user) throws Exception { | ||
return new UnsuccessfulAuthenticationAttempt(user); | ||
} | ||
|
||
private User generateUser() { | ||
User user = new User(); | ||
user.setUsername("user" + getNextInstanceNumber()); | ||
user.setEnabled(true); | ||
return user; | ||
} | ||
|
||
} |
64 changes: 64 additions & 0 deletions
64
src/main/java/org/openlmis/auth/domain/UnsuccessfulAuthenticationAttempt.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* This program is part of the OpenLMIS logistics management information system platform software. | ||
* Copyright © 2017 VillageReach | ||
* | ||
* This program is free software: you can redistribute it and/or modify it under the terms | ||
* of the GNU Affero General Public License as published by the Free Software Foundation, either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | ||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
* See the GNU Affero General Public License for more details. You should have received a copy of | ||
* the GNU Affero General Public License along with this program. If not, see | ||
* http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. | ||
*/ | ||
|
||
package org.openlmis.auth.domain; | ||
|
||
import java.time.ZonedDateTime; | ||
import javax.persistence.Column; | ||
import javax.persistence.Entity; | ||
import javax.persistence.JoinColumn; | ||
import javax.persistence.OneToOne; | ||
import javax.persistence.Table; | ||
import lombok.AllArgsConstructor; | ||
import lombok.EqualsAndHashCode; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import lombok.Setter; | ||
|
||
@Entity | ||
@Table(name = "unsuccessful_authentication_attempts") | ||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Getter | ||
@Setter | ||
@EqualsAndHashCode(callSuper = true) | ||
public class UnsuccessfulAuthenticationAttempt extends BaseEntity { | ||
|
||
@OneToOne | ||
@JoinColumn(name = "userId", nullable = false, unique = true) | ||
private User user; | ||
|
||
@Column(nullable = false, columnDefinition = "timestamp with time zone") | ||
private ZonedDateTime lastUnsuccessfulAuthenticationAttemptDate = ZonedDateTime.now(); | ||
|
||
@Column(name = "attemptcounter") | ||
private Integer attemptCounter = 0; | ||
|
||
public UnsuccessfulAuthenticationAttempt(User user) { | ||
this(); | ||
this.user = user; | ||
} | ||
|
||
public void incrementCounter() { | ||
setAttemptCounter(getAttemptCounter() + 1); | ||
setLastUnsuccessfulAuthenticationAttemptDate(ZonedDateTime.now()); | ||
} | ||
|
||
public void resetCounter() { | ||
setAttemptCounter(0); | ||
setLastUnsuccessfulAuthenticationAttemptDate(ZonedDateTime.now()); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/main/java/org/openlmis/auth/repository/UnsuccessfulAuthenticationAttemptRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* | ||
* This program is part of the OpenLMIS logistics management information system platform software. | ||
* Copyright © 2017 VillageReach | ||
* | ||
* This program is free software: you can redistribute it and/or modify it under the terms | ||
* of the GNU Affero General Public License as published by the Free Software Foundation, either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | ||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
* See the GNU Affero General Public License for more details. You should have received a copy of | ||
* the GNU Affero General Public License along with this program. If not, see | ||
* http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. | ||
*/ | ||
|
||
package org.openlmis.auth.repository; | ||
|
||
import java.util.Optional; | ||
import java.util.UUID; | ||
import org.openlmis.auth.domain.UnsuccessfulAuthenticationAttempt; | ||
import org.springframework.data.repository.CrudRepository; | ||
|
||
public interface UnsuccessfulAuthenticationAttemptRepository | ||
extends CrudRepository<UnsuccessfulAuthenticationAttempt, UUID> { | ||
|
||
Optional<UnsuccessfulAuthenticationAttempt> findByUserId(UUID userId); | ||
|
||
} |
82 changes: 82 additions & 0 deletions
82
src/main/java/org/openlmis/auth/security/OlmisAuthenticationProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* This program is part of the OpenLMIS logistics management information system platform software. | ||
* Copyright © 2017 VillageReach | ||
* | ||
* This program is free software: you can redistribute it and/or modify it under the terms | ||
* of the GNU Affero General Public License as published by the Free Software Foundation, either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | ||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
* See the GNU Affero General Public License for more details. You should have received a copy of | ||
* the GNU Affero General Public License along with this program. If not, see | ||
* http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. | ||
*/ | ||
|
||
package org.openlmis.auth.security; | ||
|
||
import java.time.Duration; | ||
import java.time.ZonedDateTime; | ||
import org.openlmis.auth.domain.UnsuccessfulAuthenticationAttempt; | ||
import org.openlmis.auth.domain.User; | ||
import org.openlmis.auth.repository.UnsuccessfulAuthenticationAttemptRepository; | ||
import org.openlmis.auth.repository.UserRepository; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.security.authentication.LockedException; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
|
||
public class OlmisAuthenticationProvider extends DaoAuthenticationProvider { | ||
|
||
@Value("${maxUnsuccessfulAuthAttempts}") | ||
private int maxUnsuccessfulAuthAttempts; | ||
|
||
@Value("${lockoutTime}") | ||
private long lockoutTime; | ||
|
||
@Autowired | ||
private UserRepository userRepository; | ||
|
||
@Autowired | ||
private UnsuccessfulAuthenticationAttemptRepository attemptCounterRepository; | ||
|
||
@Override | ||
protected void additionalAuthenticationChecks(UserDetails userDetails, | ||
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { | ||
User user = userRepository.findOneByUsernameIgnoreCase(userDetails.getUsername()); | ||
UnsuccessfulAuthenticationAttempt counter = attemptCounterRepository | ||
.findByUserId(user.getId()) | ||
.orElse(new UnsuccessfulAuthenticationAttempt(user)); | ||
boolean lockoutExpired = | ||
Duration.between(counter.getLastUnsuccessfulAuthenticationAttemptDate(), | ||
ZonedDateTime.now()).getSeconds() > lockoutTime; | ||
|
||
if (user.isLockedOut() && !lockoutExpired) { | ||
throw new LockedException("Too many failed login attempts. " | ||
+ "You can't access this page right now. Please try again later."); | ||
} else if (user.isLockedOut()) { | ||
user.setLockedOut(false); | ||
counter.resetCounter(); | ||
attemptCounterRepository.save(counter); | ||
userRepository.save(user); | ||
} | ||
|
||
try { | ||
super.additionalAuthenticationChecks(userDetails, authentication); | ||
} catch (Exception ex) { | ||
counter.incrementCounter(); | ||
|
||
if (counter.getAttemptCounter() >= maxUnsuccessfulAuthAttempts) { | ||
user.setLockedOut(true); | ||
userRepository.save(user); | ||
} | ||
attemptCounterRepository.save(counter); | ||
throw ex; | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
...ources/db/migration/20240917224024116__add_unsuccessful_authentication_attempts_table.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
CREATE TABLE unsuccessful_authentication_attempts ( | ||
id UUID PRIMARY KEY, | ||
userId UUID NOT NULL UNIQUE, | ||
lastUnsuccessfulAuthenticationAttemptDate TIMESTAMP WITH TIME ZONE NOT NULL, | ||
attemptCounter INTEGER, | ||
|
||
CONSTRAINT fk_user FOREIGN KEY (userId) REFERENCES auth_users (id) ON DELETE CASCADE | ||
); |
2 changes: 2 additions & 0 deletions
2
src/main/resources/db/migration/20240917224340535__add_locked_out_column_for_auth_users.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
ALTER TABLE auth_users | ||
ADD COLUMN lockedOut BOOLEAN DEFAULT FALSE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.