Skip to content

Commit aa97f3c

Browse files
fix: correctly reset login attempts (#13075)
Login attempts were not being reset correctly which led to situations where a failed login attempt followed by a successful login attempt would keep the loginAttempts at 1. ### Before Example with maxAttempts of 2: - failed login -> `loginAttempts: 1` - successful login -> `loginAttempts: 1` - failed login -> `loginAttempts: 2` - successful login -> `"This user is locked due to having too many failed login attempts."` ### After Example with maxAttempts of 2: - failed login -> `loginAttempts: 1` - successful login -> `loginAttempts: 0` - failed login -> `loginAttempts: 1` - successful login -> `loginAttempts: 0`
1 parent 0b88466 commit aa97f3c

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed

packages/payload/src/auth/strategies/local/resetLoginAttempts.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export const resetLoginAttempts = async ({
1515
payload,
1616
req,
1717
}: Args): Promise<void> => {
18-
if (!('lockUntil' in doc && typeof doc.lockUntil === 'string') || doc.loginAttempts === 0) {
18+
if (
19+
!('lockUntil' in doc && typeof doc.lockUntil === 'string') &&
20+
(!('loginAttempts' in doc) || doc.loginAttempts === 0)
21+
) {
1922
return
2023
}
2124
await payload.update({

test/auth/int.spec.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,136 @@ describe('Auth', () => {
10231023
}),
10241024
).rejects.toThrow('Token is either invalid or has expired.')
10251025
})
1026+
1027+
describe('Login Attempts', () => {
1028+
async function attemptLogin(email: string, password: string) {
1029+
return payload.login({
1030+
collection: slug,
1031+
data: {
1032+
email,
1033+
password,
1034+
},
1035+
overrideAccess: false,
1036+
})
1037+
}
1038+
1039+
it('should reset the login attempts after a successful login', async () => {
1040+
// fail 1
1041+
try {
1042+
const failedLogin = await attemptLogin(devUser.email, 'wrong-password')
1043+
expect(failedLogin).toBeUndefined()
1044+
} catch (error) {
1045+
expect((error as Error).message).toBe('The email or password provided is incorrect.')
1046+
}
1047+
1048+
// successful login 1
1049+
const successfulLogin = await attemptLogin(devUser.email, devUser.password)
1050+
expect(successfulLogin).toBeDefined()
1051+
1052+
// fail 2
1053+
try {
1054+
const failedLogin = await attemptLogin(devUser.email, 'wrong-password')
1055+
expect(failedLogin).toBeUndefined()
1056+
} catch (error) {
1057+
expect((error as Error).message).toBe('The email or password provided is incorrect.')
1058+
}
1059+
1060+
// successful login 2 without exceeding attempts
1061+
const successfulLogin2 = await attemptLogin(devUser.email, devUser.password)
1062+
expect(successfulLogin2).toBeDefined()
1063+
1064+
const user = await payload.findByID({
1065+
collection: slug,
1066+
id: successfulLogin2.user.id,
1067+
overrideAccess: true,
1068+
showHiddenFields: true,
1069+
})
1070+
1071+
expect(user.loginAttempts).toBe(0)
1072+
expect(user.lockUntil).toBeNull()
1073+
})
1074+
1075+
it('should lock the user after too many failed login attempts', async () => {
1076+
const now = new Date()
1077+
// fail 1
1078+
try {
1079+
const failedLogin = await attemptLogin(devUser.email, 'wrong-password')
1080+
expect(failedLogin).toBeUndefined()
1081+
} catch (error) {
1082+
expect((error as Error).message).toBe('The email or password provided is incorrect.')
1083+
}
1084+
1085+
// fail 2
1086+
try {
1087+
const failedLogin = await attemptLogin(devUser.email, 'wrong-password')
1088+
expect(failedLogin).toBeUndefined()
1089+
} catch (error) {
1090+
expect((error as Error).message).toBe('The email or password provided is incorrect.')
1091+
}
1092+
1093+
// fail 3
1094+
try {
1095+
const failedLogin = await attemptLogin(devUser.email, 'wrong-password')
1096+
expect(failedLogin).toBeUndefined()
1097+
} catch (error) {
1098+
expect((error as Error).message).toBe(
1099+
'This user is locked due to having too many failed login attempts.',
1100+
)
1101+
}
1102+
1103+
const userQuery = await payload.find({
1104+
collection: slug,
1105+
overrideAccess: true,
1106+
showHiddenFields: true,
1107+
where: {
1108+
email: {
1109+
equals: devUser.email,
1110+
},
1111+
},
1112+
})
1113+
1114+
expect(userQuery.docs[0]).toBeDefined()
1115+
1116+
if (userQuery.docs[0]) {
1117+
const user = userQuery.docs[0]
1118+
expect(user.loginAttempts).toBe(2)
1119+
expect(user.lockUntil).toBeDefined()
1120+
expect(typeof user.lockUntil).toBe('string')
1121+
if (typeof user.lockUntil === 'string') {
1122+
expect(new Date(user.lockUntil).getTime()).toBeGreaterThan(now.getTime())
1123+
}
1124+
}
1125+
})
1126+
1127+
it('should allow force unlocking of a user', async () => {
1128+
await payload.unlock({
1129+
collection: slug,
1130+
data: {
1131+
email: devUser.email,
1132+
} as any,
1133+
overrideAccess: true,
1134+
})
1135+
1136+
const userQuery = await payload.find({
1137+
collection: slug,
1138+
overrideAccess: true,
1139+
showHiddenFields: true,
1140+
where: {
1141+
email: {
1142+
equals: devUser.email,
1143+
},
1144+
},
1145+
})
1146+
1147+
expect(userQuery.docs[0]).toBeDefined()
1148+
1149+
if (userQuery.docs[0]) {
1150+
const user = userQuery.docs[0]
1151+
expect(user.loginAttempts).toBe(0)
1152+
expect(user.lockUntil).toBeNull()
1153+
}
1154+
})
1155+
})
10261156
})
10271157

10281158
describe('Email - format validation', () => {

0 commit comments

Comments
 (0)