diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 295f9b455f6..4c05931f8a8 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -16,6 +16,7 @@ import { listAuthenticationMethodTypes, listIDPLinks, listUsers, + ListUsersCommand, startIdentityProviderFlow, } from "../zitadel"; import { createSessionAndUpdateCookie } from "./cookie"; @@ -29,21 +30,22 @@ export type SendLoginnameCommand = { const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; export async function sendLoginname(command: SendLoginnameCommand) { - const users = await listUsers({ - loginName: command.loginName, + const loginSettingsByContext = await getLoginSettings(command.organization); + + let listUsersRequest: ListUsersCommand = { + userName: command.loginName, organizationId: command.organization, - }); + }; - const loginSettings = await getLoginSettings(command.organization); + if (!loginSettingsByContext?.disableLoginWithEmail) { + listUsersRequest.email = command.loginName; + } - const potentialUsers = users.result.filter((u) => { - const human = u.type.case === "human" ? u.type.value : undefined; - return loginSettings?.disableLoginWithEmail - ? human?.email?.isVerified && human?.email?.email !== command.loginName - : loginSettings?.disableLoginWithPhone - ? human?.phone?.isVerified && human?.phone?.phone !== command.loginName - : true; - }); + if (!loginSettingsByContext?.disableLoginWithPhone) { + listUsersRequest.phone = command.loginName; + } + + const { result: potentialUsers } = await listUsers(listUsersRequest); const redirectUserToSingleIDPIfAvailable = async () => { const identityProviders = await getActiveIdentityProviders( @@ -144,9 +146,16 @@ export async function sendLoginname(command: SendLoginnameCommand) { } }; - if (potentialUsers.length == 1 && potentialUsers[0].userId) { + if (potentialUsers.length > 1) { + return { error: "More than one user found. Provide a unique identifier." }; + } else if (potentialUsers.length == 1 && potentialUsers[0].userId) { + const user = potentialUsers[0]; const userId = potentialUsers[0].userId; + const userLoginSettings = await getLoginSettings( + user.details?.resourceOwner, + ); + const checks = create(ChecksSchema, { user: { search: { case: "userId", value: userId } }, }); @@ -162,7 +171,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { } // TODO: check if handling of userstate INITIAL is needed - if (potentialUsers[0].state === UserState.INITIAL) { + if (user.state === UserState.INITIAL) { return { error: "Initial User not supported" }; } @@ -172,9 +181,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if ( - potentialUsers[0].type.case === "human" && - potentialUsers[0].type.value.email && - !potentialUsers[0].type.value.email.isVerified + user.type.case === "human" && + user.type.value.email && + !user.type.value.email.isVerified ) { const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName, @@ -219,7 +228,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { const method = methods.authMethodTypes[0]; switch (method) { case AuthenticationMethodType.PASSWORD: // user has only password as auth method - if (!loginSettings?.allowUsernamePassword) { + if (!userLoginSettings?.allowUsernamePassword) { return { error: "Username Password not allowed! Contact your administrator for more information.", @@ -246,7 +255,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { }; case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY - if (loginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) { + if (userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) { return { error: "Passkeys not allowed! Contact your administrator for more information.", @@ -309,22 +318,24 @@ export async function sendLoginname(command: SendLoginnameCommand) { } } - // user not found, check if register is enabled on organization - if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) { - // TODO: do we need to handle login hints for IDPs here? + // user not found, check if register is enabled on instance / organization context + if ( + loginSettingsByContext?.allowRegister && + !loginSettingsByContext?.allowUsernamePassword + ) { const resp = await redirectUserToSingleIDPIfAvailable(); if (resp) { return resp; } return { error: "Could not find user" }; } else if ( - loginSettings?.allowRegister && - loginSettings?.allowUsernamePassword + loginSettingsByContext?.allowRegister && + loginSettingsByContext?.allowUsernamePassword ) { let orgToRegisterOn: string | undefined = command.organization; if ( - !loginSettings?.ignoreUnknownUsernames && + !loginSettingsByContext?.ignoreUnknownUsernames && !orgToRegisterOn && command.loginName && ORG_SUFFIX_REGEX.test(command.loginName) @@ -344,7 +355,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { } // do not register user if ignoreUnknownUsernames is set - if (orgToRegisterOn && !loginSettings?.ignoreUnknownUsernames) { + if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) { const params = new URLSearchParams({ organization: orgToRegisterOn }); if (command.authRequestId) { @@ -358,7 +369,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { } } - if (loginSettings?.ignoreUnknownUsernames) { + if (loginSettingsByContext?.ignoreUnknownUsernames) { const paramsPasswordDefault = new URLSearchParams({ loginName: command.loginName, }); diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 0afc4c4dc19..1858145016e 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -291,19 +291,24 @@ export async function createInviteCode(userId: string, host: string | null) { ); } -export async function listUsers({ - loginName, - userName, - email, - organizationId, -}: { +export type ListUsersCommand = { loginName?: string; userName?: string; email?: string; + phone?: string; organizationId?: string; -}) { +}; + +export async function listUsers({ + loginName, + userName, + phone, + email, + organizationId, +}: ListUsersCommand) { const queries: SearchQuery[] = []; + // either loginName or userName and email or phone are required if (loginName) { queries.push( create(SearchQuerySchema, { @@ -316,9 +321,57 @@ export async function listUsers({ }, }), ); - } + } else if (userName && (email || phone)) { + const userNameQuery = create(SearchQuerySchema, { + query: { + case: "userNameQuery", + value: { + userName: userName, + method: TextQueryMethod.EQUALS, + }, + }, + }); - if (userName) { + const orQueries: SearchQuery[] = [userNameQuery]; + + if (email) { + const emailQuery = create(SearchQuerySchema, { + query: { + case: "emailQuery", + value: { + emailAddress: email, + method: TextQueryMethod.EQUALS, + }, + }, + }); + orQueries.push(emailQuery); + } + + if (phone) { + // TODO add after https://github.com/zitadel/zitadel/issues/9016 is merged + // const phoneQuery = create(SearchQuerySchema, { + // query: { + // case: "phoneQuery", + // value: { + // emailAddress: email, + // method: TextQueryMethod.EQUALS, + // }, + // }, + // }); + // orQueries.push(phoneQuery); + } + + queries.push( + create(SearchQuerySchema, { + query: { + case: "orQuery", + value: { + queries: orQueries, + }, + }, + }), + ); + } else if (userName) { queries.push( create(SearchQuerySchema, { query: { @@ -345,19 +398,6 @@ export async function listUsers({ ); } - if (email) { - queries.push( - create(SearchQuerySchema, { - query: { - case: "emailQuery", - value: { - emailAddress: email, - }, - }, - }), - ); - } - return userService.listUsers({ queries: queries }); }