diff --git a/test/api/v3/integration/user/auth/POST-login-local.test.js b/test/api/v3/integration/user/auth/POST-login-local.test.js index 62662be1646..a58c0df3705 100644 --- a/test/api/v3/integration/user/auth/POST-login-local.test.js +++ b/test/api/v3/integration/user/auth/POST-login-local.test.js @@ -28,6 +28,15 @@ describe('POST /user/auth/local/login', () => { expect(response.apiToken).to.eql(user.apiToken); }); + it('success with case-insensitive username', async () => { + user = await generateUser({}, { username: 'CASE-INSENSITIVE' }); + const response = await api.post(endpoint, { + username: 'Case-Insensitive', + password, + }); + expect(response.apiToken).to.eql(user.apiToken); + }); + it('success with email', async () => { const response = await api.post(endpoint, { username: user.auth.local.email, diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index 31cf12408e7..dac92e2fa16 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -99,11 +99,21 @@ api.loginLocal = { if (validator.isEmail(String(username))) { login = { 'auth.local.email': username.toLowerCase() }; // Emails are stored lowercase } else { - login = { 'auth.local.username': username }; + login = { 'auth.local.lowerCaseUsername': username.toLowerCase() }; } - // load the entire user because we may have to save it to convert the password to bcrypt - const user = await User.findOne(login).exec(); + // load the entire user because we may have to save it to convert the password to bcrypt. + // lookup the user by case-insensitive username. + // there's a rare chance for duplicates before registration enforced case-insensitive uniqueness + const potentialUsers = await User.find(login).exec(); + let user; + + if (potentialUsers.length > 1) { + // multiple users share the same username. fallback to case-sensitive username matching + user = potentialUsers.find(u => u.auth.local.username === username); + } else { + [user] = potentialUsers; + } // if user is using social login, then user will not have a hashed_password stored if (!user || !user.auth.local.hashed_password) throw new NotAuthorized(res.t('invalidLoginCredentialsLong'));