From 5e44b719403a72861b1eaf78e1ec0c6a9549050b Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:20:01 +0200 Subject: [PATCH] feat(API/login): permissive email handling Allow case insensitive email when there's no other candidate. closes #6570 --- packages/tests/src/api/users/oauth.ts | 15 ++++++++++++++- server/core/lib/auth/oauth-model.ts | 13 ++++++++++--- server/core/models/user/user.ts | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/tests/src/api/users/oauth.ts b/packages/tests/src/api/users/oauth.ts index fe50872cb..154a0dc56 100644 --- a/packages/tests/src/api/users/oauth.ts +++ b/packages/tests/src/api/users/oauth.ts @@ -28,6 +28,8 @@ describe('Test oauth', function () { }) await setAccessTokensToServers([ server ]) + await server.users.create({ username: 'user1', email: 'user@example.com' }) + await server.users.create({ username: 'user2', email: 'User@example.com', password: 'AdvancedPassword' }) sqlCommand = new SQLCommand(server) }) @@ -79,7 +81,7 @@ describe('Test oauth', function () { }) it('Should not login with an invalid password', async function () { - const user = { username: server.store.user.username, password: 'mew_three' } + const user = { username: 'User@example.com', password: 'password' } const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) expectInvalidCredentials(body) @@ -87,6 +89,9 @@ describe('Test oauth', function () { it('Should be able to login', async function () { await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) + + const user = { username: 'User@example.com', password: 'AdvancedPassword' } + await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) }) it('Should be able to login with an insensitive username', async function () { @@ -99,6 +104,14 @@ describe('Test oauth', function () { const user3 = { username: 'ROOt', password: server.store.user.password } await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) }) + + it('Should be able to login with an insensitive email when no similar emails exist', async function () { + const user = { username: 'ADMIN' + server.internalServerNumber + '@example.com', password: server.store.user.password } + await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) + + const user2 = { username: 'admin' + server.internalServerNumber + '@example.com', password: server.store.user.password } + await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) + }) }) describe('Logout', function () { diff --git a/server/core/lib/auth/oauth-model.ts b/server/core/lib/auth/oauth-model.ts index e801b678b..ce1ba6fb4 100644 --- a/server/core/lib/auth/oauth-model.ts +++ b/server/core/lib/auth/oauth-model.ts @@ -14,7 +14,7 @@ import { OAuthClientModel } from '../../models/oauth/oauth-client.js' import { OAuthTokenModel } from '../../models/oauth/oauth-token.js' import { UserModel } from '../../models/user/user.js' import { findAvailableLocalActorName } from '../local-actor.js' -import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user.js' +import { buildUser, createUserAccountAndChannelAndPlaylist, getUserByEmailPermissive } from '../user.js' import { ExternalUser } from './external-auth.js' import { TokensCache } from './tokens-cache.js' @@ -87,7 +87,7 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin if (bypassLogin && bypassLogin.bypass === true) { logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) - let user = await UserModel.loadByEmail(bypassLogin.user.email) + let user = getUserByEmailPermissive(await UserModel.loadByEmailCaseInsensitive(bypassLogin.user.email), bypassLogin.user.email) if (!user) { user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) @@ -119,7 +119,14 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') - const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) + const users = await UserModel.loadByUsernameOrEmailCaseInsensitive(usernameOrEmail) + let user: MUserDefault + + if (usernameOrEmail.includes('@')) { + user = getUserByEmailPermissive(users, usernameOrEmail) + } else { + user = users[0] + } // If we don't find the user, or if the user belongs to a plugin if (!user || user.pluginAuth !== null || !password) return null diff --git a/server/core/models/user/user.ts b/server/core/models/user/user.ts index eaf7687a4..4d23f800e 100644 --- a/server/core/models/user/user.ts +++ b/server/core/models/user/user.ts @@ -701,6 +701,20 @@ export class UserModel extends SequelizeModel { return UserModel.findOne(query) } + static loadByUsernameOrEmailCaseInsensitive (usernameOrEmail: string): Promise { + const query = { + where: { + [Op.or]: [ + where(fn('lower', col('username')), fn('lower', usernameOrEmail) as any), + + where(fn('lower', col('email')), fn('lower', usernameOrEmail) as any) + ] + } + } + + return UserModel.findAll(query) + } + static loadByVideoId (videoId: number): Promise { const query = { include: [