forked from devondragon/SpringUserFramework
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLoginAttemptService.java
More file actions
139 lines (128 loc) · 4.42 KB
/
LoginAttemptService.java
File metadata and controls
139 lines (128 loc) · 4.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package com.digitalsanctuary.spring.user.service;
import java.time.Duration;
import java.time.Instant;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.persistence.repository.UserRepository;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Service for tracking login attempts and implementing account lockout protection.
*
* <p>Tracks successful and failed login attempts per user account. When failed attempts exceed the
* configured threshold ({@code user.security.failedLoginAttempts}), the account is locked for the
* duration specified by {@code user.security.accountLockoutDuration}.</p>
*
* <p>For IP-based blocking and rate limiting, see Bucket4J and the Bucket4J Spring Boot Starter.
* More information: <a href="https://github.com/devondragon/SpringUserFramework/issues/57">GitHub Issue #57</a></p>
*
* @see User#failedLoginAttempts
* @see User#locked
*/
@Slf4j
@RequiredArgsConstructor
@Service
@Data
public class LoginAttemptService {
final private UserRepository userRepository;
/** The max failed login attempts on a given account before it is locked. A value of 0 will disable locking accounts based on failed logins. */
@Value("${user.security.failedLoginAttempts}")
private int maxFailedLoginAttempts;
/**
* The account lockout duration. A value less than 0 means accounts can only be unlocked by action, not duration. A value of 0 means account
* lockouts are disabled. A value greater than 0 is the number of minutes that an account will stay locked before automatically unlocking.
*/
@Value("${user.security.accountLockoutDuration}")
private int accountLockoutDuration;
/**
* Login succeeded, reset failed login attempts.
*
* @param email the email address of the user
*/
@Transactional
public void loginSucceeded(final String email) {
log.debug("Login succeeded for user: {}", email);
User user = userRepository.findByEmail(email);
if (user != null) {
user.setFailedLoginAttempts(0);
user.setLocked(false);
user.setLockedDate(null);
userRepository.save(user);
}
}
/**
* Login failed.
*
* @param email the email address of the user
*/
@Transactional
public void loginFailed(final String email) {
log.debug("Login attempt failed for user: {}", email);
if (maxFailedLoginAttempts > 0) {
User user = userRepository.findByEmail(email);
if (user != null) {
incrementFailedAttempts(user);
} else {
log.warn("User not found for email: {}", email);
}
}
}
/**
* Increment failed attempts.
*
* @param user the user
*/
private void incrementFailedAttempts(User user) {
int currentAttempts = user.getFailedLoginAttempts();
user.setFailedLoginAttempts(++currentAttempts);
if (currentAttempts >= maxFailedLoginAttempts) {
user.setLocked(true);
user.setLockedDate(Instant.now());
}
userRepository.save(user);
}
/**
* Checks if the user account is locked.
*
* @param email the email address (which is the username) of the user
* @return true, if the user account is currently locked
*/
public boolean isLocked(final String email) {
log.debug("Checking if user is locked: {}", email);
User user = userRepository.findByEmail(email);
if (user != null && user.isLocked()) {
// See if the user will be automatically unlocked
user = checkIfUserShouldBeUnlocked(user);
// If the user is still locked, return true
if (user != null && user.isLocked()) {
log.debug("User is locked: {}", email);
return true;
}
}
log.debug("User is not locked: {}", email);
return false;
}
/**
* Check if user should be unlocked, and unlock the user if necessary.
*
* @param user the user
* @return the user
*/
public User checkIfUserShouldBeUnlocked(User user) {
log.debug("Checking if user should be unlocked: {}", user.getEmail());
if (user.isLocked() && user.getLockedDate() != null && accountLockoutDuration >= 0) {
long diffMinutes = Duration.between(user.getLockedDate(), Instant.now()).toMinutes();
if (diffMinutes >= accountLockoutDuration) {
log.debug("User should be unlocked: {}", user.getEmail());
user.setLocked(false);
user.setLockedDate(null);
user.setFailedLoginAttempts(0);
userRepository.save(user);
}
}
return user;
}
}