mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-14 06:29:20 +00:00
Merge branch 'main' into dependabot/github_actions/actions/setup-node-4
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
zitadel:
|
zitadel:
|
||||||
user: "${ZITADEL_DEV_UID}"
|
user: "${ZITADEL_DEV_UID}"
|
||||||
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.65.0}"
|
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.67.2}"
|
||||||
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
|
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
@@ -40,6 +40,7 @@ echo "ZITADEL_API_URL=${ZITADEL_API_URL}
|
|||||||
ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID}
|
ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID}
|
||||||
ZITADEL_SERVICE_USER_TOKEN=${PAT}
|
ZITADEL_SERVICE_USER_TOKEN=${PAT}
|
||||||
SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
|
SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
|
||||||
|
EMAIL_VERIFICATION=true
|
||||||
DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null
|
DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null
|
||||||
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
|
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
|
||||||
cat ${WRITE_ENVIRONMENT_FILE}
|
cat ${WRITE_ENVIRONMENT_FILE}
|
||||||
|
12
acceptance/tests/email-verify-screen.ts
Normal file
12
acceptance/tests/email-verify-screen.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const codeTextInput = "code-text-input";
|
||||||
|
|
||||||
|
export async function emailVerifyScreen(page: Page, code: string) {
|
||||||
|
await page.getByTestId(codeTextInput).pressSequentially(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function emailVerifyScreenExpect(page: Page, code: string) {
|
||||||
|
await expect(page.getByTestId(codeTextInput)).toHaveValue(code);
|
||||||
|
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify email");
|
||||||
|
}
|
73
acceptance/tests/email-verify.spec.ts
Normal file
73
acceptance/tests/email-verify.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { emailVerify, emailVerifyResend } from "./email-verify";
|
||||||
|
import { emailVerifyScreenExpect } from "./email-verify-screen";
|
||||||
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
|
import { getCodeFromSink } from "./sink";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: false,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, verify", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
// auto-redirect on /verify
|
||||||
|
// wait for send of the code
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const c = await getCodeFromSink(user.getUsername());
|
||||||
|
await emailVerify(page, c);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, resend, verify", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
// auto-redirect on /verify
|
||||||
|
await emailVerifyResend(page);
|
||||||
|
// wait for send of the code
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const c = await getCodeFromSink(user.getUsername());
|
||||||
|
await emailVerify(page, c);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, resend, old code", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
// auto-redirect on /verify
|
||||||
|
// wait for send of the code
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const c = await getCodeFromSink(user.getUsername());
|
||||||
|
await emailVerifyResend(page);
|
||||||
|
// wait for resend of the code
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await emailVerify(page, c);
|
||||||
|
await emailVerifyScreenExpect(page, c);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, wrong code", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
// auto-redirect on /verify
|
||||||
|
const code = "wrong";
|
||||||
|
await emailVerify(page, code);
|
||||||
|
await emailVerifyScreenExpect(page, code);
|
||||||
|
});
|
15
acceptance/tests/email-verify.ts
Normal file
15
acceptance/tests/email-verify.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { emailVerifyScreen } from "./email-verify-screen";
|
||||||
|
|
||||||
|
export async function startEmailVerify(page: Page, loginname: string) {
|
||||||
|
await page.goto("/verify");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function emailVerify(page: Page, code: string) {
|
||||||
|
await emailVerifyScreen(page, code);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function emailVerifyResend(page: Page) {
|
||||||
|
await page.getByTestId("resend-button").click();
|
||||||
|
}
|
@@ -4,6 +4,10 @@ import { getCodeFromSink } from "./sink";
|
|||||||
const codeField = "code-text-input";
|
const codeField = "code-text-input";
|
||||||
const passwordField = "password-text-input";
|
const passwordField = "password-text-input";
|
||||||
const passwordConfirmField = "password-confirm-text-input";
|
const passwordConfirmField = "password-confirm-text-input";
|
||||||
|
const passwordChangeField = "password-change-text-input";
|
||||||
|
const passwordChangeConfirmField = "password-change-confirm-text-input";
|
||||||
|
const passwordSetField = "password-set-text-input";
|
||||||
|
const passwordSetConfirmField = "password-set-confirm-text-input";
|
||||||
const lengthCheck = "length-check";
|
const lengthCheck = "length-check";
|
||||||
const symbolCheck = "symbol-check";
|
const symbolCheck = "symbol-check";
|
||||||
const numberCheck = "number-check";
|
const numberCheck = "number-check";
|
||||||
@@ -15,8 +19,8 @@ const matchText = "Matches";
|
|||||||
const noMatchText = "Doesn't match";
|
const noMatchText = "Doesn't match";
|
||||||
|
|
||||||
export async function changePasswordScreen(page: Page, password1: string, password2: string) {
|
export async function changePasswordScreen(page: Page, password1: string, password2: string) {
|
||||||
await page.getByTestId(passwordField).pressSequentially(password1);
|
await page.getByTestId(passwordChangeField).pressSequentially(password1);
|
||||||
await page.getByTestId(passwordConfirmField).pressSequentially(password2);
|
await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function passwordScreen(page: Page, password: string) {
|
export async function passwordScreen(page: Page, password: string) {
|
||||||
@@ -39,9 +43,21 @@ export async function changePasswordScreenExpect(
|
|||||||
lowercase: boolean,
|
lowercase: boolean,
|
||||||
equals: boolean,
|
equals: boolean,
|
||||||
) {
|
) {
|
||||||
await expect(page.getByTestId(passwordField)).toHaveValue(password1);
|
await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1);
|
||||||
await expect(page.getByTestId(passwordConfirmField)).toHaveValue(password2);
|
await expect(page.getByTestId(passwordChangeConfirmField)).toHaveValue(password2);
|
||||||
|
|
||||||
|
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkComplexity(
|
||||||
|
page: Page,
|
||||||
|
length: boolean,
|
||||||
|
symbol: boolean,
|
||||||
|
number: boolean,
|
||||||
|
uppercase: boolean,
|
||||||
|
lowercase: boolean,
|
||||||
|
equals: boolean,
|
||||||
|
) {
|
||||||
await checkContent(page, lengthCheck, length);
|
await checkContent(page, lengthCheck, length);
|
||||||
await checkContent(page, symbolCheck, symbol);
|
await checkContent(page, symbolCheck, symbol);
|
||||||
await checkContent(page, numberCheck, number);
|
await checkContent(page, numberCheck, number);
|
||||||
@@ -63,8 +79,8 @@ export async function resetPasswordScreen(page: Page, username: string, password
|
|||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
const c = await getCodeFromSink(username);
|
const c = await getCodeFromSink(username);
|
||||||
await page.getByTestId(codeField).pressSequentially(c);
|
await page.getByTestId(codeField).pressSequentially(c);
|
||||||
await page.getByTestId(passwordField).pressSequentially(password1);
|
await page.getByTestId(passwordSetField).pressSequentially(password1);
|
||||||
await page.getByTestId(passwordConfirmField).pressSequentially(password2);
|
await page.getByTestId(passwordSetConfirmField).pressSequentially(password2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetPasswordScreenExpect(
|
export async function resetPasswordScreenExpect(
|
||||||
@@ -78,5 +94,8 @@ export async function resetPasswordScreenExpect(
|
|||||||
lowercase: boolean,
|
lowercase: boolean,
|
||||||
equals: boolean,
|
equals: boolean,
|
||||||
) {
|
) {
|
||||||
await changePasswordScreenExpect(page, password1, password2, length, symbol, number, uppercase, lowercase, equals);
|
await expect(page.getByTestId(passwordSetField)).toHaveValue(password1);
|
||||||
|
await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2);
|
||||||
|
|
||||||
|
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
|
||||||
}
|
}
|
||||||
|
@@ -8,8 +8,7 @@ export async function startChangePassword(page: Page, loginname: string) {
|
|||||||
await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname }));
|
await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changePassword(page: Page, loginname: string, password: string) {
|
export async function changePassword(page: Page, password: string) {
|
||||||
await startChangePassword(page, loginname);
|
|
||||||
await changePasswordScreen(page, password, password);
|
await changePasswordScreen(page, password, password);
|
||||||
await page.getByTestId(passwordSubmitButton).click();
|
await page.getByTestId(passwordSubmitButton).click();
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { Page } from "@playwright/test";
|
import { Page } from "@playwright/test";
|
||||||
|
import { emailVerify } from "./email-verify";
|
||||||
import { passkeyRegister } from "./passkey";
|
import { passkeyRegister } from "./passkey";
|
||||||
import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
|
import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
|
||||||
|
import { getCodeFromSink } from "./sink";
|
||||||
|
|
||||||
export async function registerWithPassword(
|
export async function registerWithPassword(
|
||||||
page: Page,
|
page: Page,
|
||||||
@@ -15,6 +17,9 @@ export async function registerWithPassword(
|
|||||||
await page.getByTestId("submit-button").click();
|
await page.getByTestId("submit-button").click();
|
||||||
await registerPasswordScreen(page, password1, password2);
|
await registerPasswordScreen(page, password1, password2);
|
||||||
await page.getByTestId("submit-button").click();
|
await page.getByTestId("submit-button").click();
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
await verifyEmail(page, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise<string> {
|
export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise<string> {
|
||||||
@@ -23,7 +28,15 @@ export async function registerWithPasskey(page: Page, firstname: string, lastnam
|
|||||||
await page.getByTestId("submit-button").click();
|
await page.getByTestId("submit-button").click();
|
||||||
|
|
||||||
// wait for projection of user
|
// wait for projection of user
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(3000);
|
||||||
|
const authId = await passkeyRegister(page);
|
||||||
|
|
||||||
return await passkeyRegister(page);
|
await verifyEmail(page, email);
|
||||||
|
return authId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyEmail(page: Page, email: string) {
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const c = await getCodeFromSink(email);
|
||||||
|
await emailVerify(page, c);
|
||||||
}
|
}
|
||||||
|
@@ -4,11 +4,14 @@ import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./
|
|||||||
|
|
||||||
export interface userProps {
|
export interface userProps {
|
||||||
email: string;
|
email: string;
|
||||||
|
isEmailVerified?: boolean;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
passwordChangeRequired?: boolean;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
isPhoneVerified?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
@@ -77,11 +80,14 @@ export enum OtpType {
|
|||||||
|
|
||||||
export interface otpUserProps {
|
export interface otpUserProps {
|
||||||
email: string;
|
email: string;
|
||||||
|
isEmailVerified?: boolean;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
passwordChangeRequired?: boolean;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
isPhoneVerified?: boolean;
|
||||||
type: OtpType;
|
type: OtpType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +102,9 @@ export class PasswordUserWithOTP extends User {
|
|||||||
organization: props.organization,
|
organization: props.organization,
|
||||||
password: props.password,
|
password: props.password,
|
||||||
phone: props.phone,
|
phone: props.phone,
|
||||||
|
isEmailVerified: props.isEmailVerified,
|
||||||
|
isPhoneVerified: props.isPhoneVerified,
|
||||||
|
passwordChangeRequired: props.passwordChangeRequired,
|
||||||
});
|
});
|
||||||
this.type = props.type;
|
this.type = props.type;
|
||||||
}
|
}
|
||||||
@@ -133,6 +142,8 @@ export interface passkeyUserProps {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
isEmailVerified?: boolean;
|
||||||
|
isPhoneVerified?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PasskeyUser extends User {
|
export class PasskeyUser extends User {
|
||||||
@@ -146,6 +157,8 @@ export class PasskeyUser extends User {
|
|||||||
organization: props.organization,
|
organization: props.organization,
|
||||||
password: "",
|
password: "",
|
||||||
phone: props.phone,
|
phone: props.phone,
|
||||||
|
isEmailVerified: props.isEmailVerified,
|
||||||
|
isPhoneVerified: props.isPhoneVerified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,10 +12,12 @@ const test = base.extend<{ user: PasskeyUser }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasskeyUser({
|
const user = new PasskeyUser({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number(),
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
});
|
});
|
||||||
await user.ensure(page);
|
await user.ensure(page);
|
||||||
await use(user);
|
await use(user);
|
||||||
|
41
acceptance/tests/username-password-change-required.spec.ts
Normal file
41
acceptance/tests/username-password-change-required.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
|
import { changePassword } from "./password";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: true,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, change required", async ({ user, page }) => {
|
||||||
|
const changedPw = "ChangedPw1!";
|
||||||
|
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
await changePassword(page, changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
|
||||||
|
await loginWithPassword(page, user.getUsername(), changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
@@ -2,8 +2,8 @@ import { faker } from "@faker-js/faker";
|
|||||||
import { test as base } from "@playwright/test";
|
import { test as base } from "@playwright/test";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { loginWithPassword } from "./login";
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
import { startChangePassword } from "./password";
|
import { changePassword, startChangePassword } from "./password";
|
||||||
import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
|
import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
|
||||||
import { PasswordUser } from "./user";
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUser }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasswordUser({
|
const user = new PasswordUser({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number(),
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
password: "Password1!",
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
});
|
});
|
||||||
await user.ensure(page);
|
await user.ensure(page);
|
||||||
await use(user);
|
await use(user);
|
||||||
@@ -27,20 +30,18 @@ const test = base.extend<{ user: PasswordUser }>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("username and password changed login", async ({ user, page }) => {
|
test("username and password changed login", async ({ user, page }) => {
|
||||||
// commented, fix in https://github.com/zitadel/zitadel/pull/8807
|
|
||||||
/*
|
|
||||||
const changedPw = "ChangedPw1!";
|
const changedPw = "ChangedPw1!";
|
||||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
|
||||||
// wait for projection of token
|
// wait for projection of token
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
await changePassword(page, user.getUsername(), changedPw);
|
await startChangePassword(page, user.getUsername());
|
||||||
|
await changePassword(page, changedPw);
|
||||||
await loginScreenExpect(page, user.getFullName());
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
|
||||||
await loginWithPassword(page, user.getUsername(), changedPw);
|
await loginWithPassword(page, user.getUsername(), changedPw);
|
||||||
await loginScreenExpect(page, user.getFullName());
|
await loginScreenExpect(page, user.getFullName());
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("password change not with desired complexity", async ({ user, page }) => {
|
test("password change not with desired complexity", async ({ user, page }) => {
|
||||||
|
@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasswordUserWithOTP({
|
const user = new PasswordUserWithOTP({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number(),
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
password: "Password1!",
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
type: OtpType.email,
|
type: OtpType.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasswordUserWithOTP({
|
const user = new PasswordUserWithOTP({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number({ style: "international" }),
|
phone: faker.phone.number({ style: "international" }),
|
||||||
|
isPhoneVerified: true,
|
||||||
password: "Password1!",
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
type: OtpType.sms,
|
type: OtpType.sms,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -15,11 +15,14 @@ const test = base.extend<{ user: PasswordUser }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasswordUser({
|
const user = new PasswordUser({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number(),
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
password: "Password1!",
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
});
|
});
|
||||||
await user.ensure(page);
|
await user.ensure(page);
|
||||||
await use(user);
|
await use(user);
|
||||||
@@ -28,8 +31,6 @@ const test = base.extend<{ user: PasswordUser }>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("username and password set login", async ({ user, page }) => {
|
test("username and password set login", async ({ user, page }) => {
|
||||||
// commented, fix in https://github.com/zitadel/zitadel/pull/8807
|
|
||||||
|
|
||||||
const changedPw = "ChangedPw1!";
|
const changedPw = "ChangedPw1!";
|
||||||
await startLogin(page);
|
await startLogin(page);
|
||||||
await loginname(page, user.getUsername());
|
await loginname(page, user.getUsername());
|
||||||
|
@@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasswordUserWithTOTP({
|
const user = new PasswordUserWithTOTP({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number({ style: "international" }),
|
phone: faker.phone.number({ style: "international" }),
|
||||||
|
isPhoneVerified: true,
|
||||||
password: "Password1!",
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await user.ensure(page);
|
await user.ensure(page);
|
||||||
|
@@ -16,11 +16,14 @@ const test = base.extend<{ user: PasswordUser }>({
|
|||||||
user: async ({ page }, use) => {
|
user: async ({ page }, use) => {
|
||||||
const user = new PasswordUser({
|
const user = new PasswordUser({
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
organization: "",
|
organization: "",
|
||||||
phone: faker.phone.number(),
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
password: "Password1!",
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
});
|
});
|
||||||
await user.ensure(page);
|
await user.ensure(page);
|
||||||
await use(user);
|
await use(user);
|
||||||
|
@@ -19,13 +19,20 @@ export async function addUser(props: userProps) {
|
|||||||
isVerified: true,
|
isVerified: true,
|
||||||
},
|
},
|
||||||
phone: {
|
phone: {
|
||||||
phone: props.phone!,
|
phone: props.phone,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
password: props.password!,
|
password: props.password,
|
||||||
|
changeRequired: props.passwordChangeRequired ?? false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (!props.isEmailVerified) {
|
||||||
|
delete body.email.isVerified;
|
||||||
|
}
|
||||||
|
if (!props.isPhoneVerified) {
|
||||||
|
delete body.phone.isVerified;
|
||||||
|
}
|
||||||
|
|
||||||
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body);
|
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body);
|
||||||
}
|
}
|
||||||
|
@@ -395,6 +395,5 @@ Timebased features like the multifactor init prompt or password expiry, are not
|
|||||||
- Password Expiry Settings
|
- Password Expiry Settings
|
||||||
- Login Settings: multifactor init prompt
|
- Login Settings: multifactor init prompt
|
||||||
- forceMFA on login settings is not checked for IDPs
|
- forceMFA on login settings is not checked for IDPs
|
||||||
- disablePhone / disableEmail from loginSettings will be implemented right after https://github.com/zitadel/zitadel/issues/9016 is merged
|
|
||||||
|
|
||||||
Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced.
|
Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced.
|
||||||
|
@@ -20,6 +20,7 @@ export default async function Page(props: {
|
|||||||
const loginName = searchParams?.loginName;
|
const loginName = searchParams?.loginName;
|
||||||
const authRequestId = searchParams?.authRequestId;
|
const authRequestId = searchParams?.authRequestId;
|
||||||
const organization = searchParams?.organization;
|
const organization = searchParams?.organization;
|
||||||
|
const suffix = searchParams?.suffix;
|
||||||
const submit: boolean = searchParams?.submit === "true";
|
const submit: boolean = searchParams?.submit === "true";
|
||||||
|
|
||||||
let defaultOrganization;
|
let defaultOrganization;
|
||||||
@@ -34,6 +35,8 @@ export default async function Page(props: {
|
|||||||
organization ?? defaultOrganization,
|
organization ?? defaultOrganization,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const contextLoginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
const identityProviders = await getActiveIdentityProviders(
|
const identityProviders = await getActiveIdentityProviders(
|
||||||
organization ?? defaultOrganization,
|
organization ?? defaultOrganization,
|
||||||
).then((resp) => {
|
).then((resp) => {
|
||||||
@@ -54,6 +57,8 @@ export default async function Page(props: {
|
|||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
|
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
|
||||||
|
loginSettings={contextLoginSettings}
|
||||||
|
suffix={suffix}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
allowRegister={!!loginSettings?.allowRegister}
|
allowRegister={!!loginSettings?.allowRegister}
|
||||||
>
|
>
|
||||||
|
@@ -300,6 +300,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const { authRequest } = await getAuthRequest({ authRequestId });
|
const { authRequest } = await getAuthRequest({ authRequestId });
|
||||||
|
|
||||||
let organization = "";
|
let organization = "";
|
||||||
|
let suffix = "";
|
||||||
let idpId = "";
|
let idpId = "";
|
||||||
|
|
||||||
if (authRequest?.scope) {
|
if (authRequest?.scope) {
|
||||||
@@ -326,6 +327,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const orgs = await getOrgsByDomain(orgDomain);
|
const orgs = await getOrgsByDomain(orgDomain);
|
||||||
if (orgs.result && orgs.result.length === 1) {
|
if (orgs.result && orgs.result.length === 1) {
|
||||||
organization = orgs.result[0].id ?? "";
|
organization = orgs.result[0].id ?? "";
|
||||||
|
suffix = orgDomain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,6 +450,9 @@ export async function GET(request: NextRequest) {
|
|||||||
if (organization) {
|
if (organization) {
|
||||||
loginNameUrl.searchParams.set("organization", organization);
|
loginNameUrl.searchParams.set("organization", organization);
|
||||||
}
|
}
|
||||||
|
if (suffix) {
|
||||||
|
loginNameUrl.searchParams.set("suffix", suffix);
|
||||||
|
}
|
||||||
return NextResponse.redirect(loginNameUrl);
|
return NextResponse.redirect(loginNameUrl);
|
||||||
} else if (authRequest.prompt.includes(Prompt.NONE)) {
|
} else if (authRequest.prompt.includes(Prompt.NONE)) {
|
||||||
/**
|
/**
|
||||||
|
@@ -161,7 +161,7 @@ export function ChangePasswordForm({
|
|||||||
})}
|
})}
|
||||||
label="New Password"
|
label="New Password"
|
||||||
error={errors.password?.message as string}
|
error={errors.password?.message as string}
|
||||||
data-testid="password-text-input"
|
data-testid="password-change-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -174,7 +174,7 @@ export function ChangePasswordForm({
|
|||||||
})}
|
})}
|
||||||
label="Confirm Password"
|
label="Confirm Password"
|
||||||
error={errors.confirmPassword?.message as string}
|
error={errors.confirmPassword?.message as string}
|
||||||
data-testid="password-confirm-text-input"
|
data-testid="password-change-confirm-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -15,6 +15,7 @@ export type TextInputProps = DetailedHTMLProps<
|
|||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> & {
|
> & {
|
||||||
label: string;
|
label: string;
|
||||||
|
suffix?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
error?: string | ReactNode;
|
error?: string | ReactNode;
|
||||||
@@ -45,6 +46,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
suffix,
|
||||||
required = false,
|
required = false,
|
||||||
error,
|
error,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -56,7 +58,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<label className="flex flex-col text-12px text-input-light-label dark:text-input-dark-label">
|
<label className="relative flex flex-col text-12px text-input-light-label dark:text-input-dark-label">
|
||||||
<span
|
<span
|
||||||
className={`leading-3 mb-1 ${
|
className={`leading-3 mb-1 ${
|
||||||
error ? "text-warn-light-500 dark:text-warn-dark-500" : ""
|
error ? "text-warn-light-500 dark:text-warn-dark-500" : ""
|
||||||
@@ -78,6 +80,12 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{suffix && (
|
||||||
|
<span className="z-30 absolute right-[3px] bottom-[22px] transform translate-y-1/2 bg-background-light-500 dark:bg-background-dark-500 p-2 rounded-sm">
|
||||||
|
@{suffix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="leading-14.5px h-14.5px text-warn-light-500 dark:text-warn-dark-500 flex flex-row items-center text-12px">
|
<div className="leading-14.5px h-14.5px text-warn-light-500 dark:text-warn-dark-500 flex flex-row items-center text-12px">
|
||||||
<span>{error ? error : " "}</span>
|
<span>{error ? error : " "}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -237,7 +237,7 @@ export function SetPasswordForm({
|
|||||||
})}
|
})}
|
||||||
label="New Password"
|
label="New Password"
|
||||||
error={errors.password?.message as string}
|
error={errors.password?.message as string}
|
||||||
data-testid="password-text-input"
|
data-testid="password-set-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -250,7 +250,7 @@ export function SetPasswordForm({
|
|||||||
})}
|
})}
|
||||||
label="Confirm Password"
|
label="Confirm Password"
|
||||||
error={errors.confirmPassword?.message as string}
|
error={errors.confirmPassword?.message as string}
|
||||||
data-testid="password-confirm-text-input"
|
data-testid="password-set-confirm-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { sendLoginname } from "@/lib/server/loginname";
|
import { sendLoginname } from "@/lib/server/loginname";
|
||||||
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
@@ -18,7 +19,9 @@ type Inputs = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
loginName: string | undefined;
|
loginName: string | undefined;
|
||||||
authRequestId: string | undefined;
|
authRequestId: string | undefined;
|
||||||
|
loginSettings: LoginSettings | undefined;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
suffix?: string;
|
||||||
submit: boolean;
|
submit: boolean;
|
||||||
allowRegister: boolean;
|
allowRegister: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -28,6 +31,8 @@ export function UsernameForm({
|
|||||||
loginName,
|
loginName,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
organization,
|
organization,
|
||||||
|
suffix,
|
||||||
|
loginSettings,
|
||||||
submit,
|
submit,
|
||||||
allowRegister,
|
allowRegister,
|
||||||
children,
|
children,
|
||||||
@@ -52,6 +57,7 @@ export function UsernameForm({
|
|||||||
loginName: values.loginName,
|
loginName: values.loginName,
|
||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
suffix,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("An internal error occurred");
|
setError("An internal error occurred");
|
||||||
@@ -80,6 +86,18 @@ export function UsernameForm({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
let inputLabel = "Loginname";
|
||||||
|
if (
|
||||||
|
loginSettings?.disableLoginWithEmail &&
|
||||||
|
loginSettings?.disableLoginWithPhone
|
||||||
|
) {
|
||||||
|
inputLabel = "Username";
|
||||||
|
} else if (loginSettings?.disableLoginWithEmail) {
|
||||||
|
inputLabel = "Username or phone number";
|
||||||
|
} else if (loginSettings?.disableLoginWithPhone) {
|
||||||
|
inputLabel = "Username or email";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="w-full">
|
<form className="w-full">
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -87,8 +105,9 @@ export function UsernameForm({
|
|||||||
type="text"
|
type="text"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
{...register("loginName", { required: "This field is required" })}
|
{...register("loginName", { required: "This field is required" })}
|
||||||
label="Loginname"
|
label={inputLabel}
|
||||||
data-testid="username-text-input"
|
data-testid="username-text-input"
|
||||||
|
suffix={suffix}
|
||||||
/>
|
/>
|
||||||
{allowRegister && (
|
{allowRegister && (
|
||||||
<button
|
<button
|
||||||
|
@@ -115,7 +115,7 @@ export function VerifyForm({
|
|||||||
{t("verify.noCodeReceived")}
|
{t("verify.noCodeReceived")}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
aria-label="Resend OTP Code"
|
aria-label="Resend Code"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
|
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
|
||||||
@@ -134,11 +134,12 @@ export function VerifyForm({
|
|||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
{...register("code", { required: "This field is required" })}
|
{...register("code", { required: "This field is required" })}
|
||||||
label="Code"
|
label="Code"
|
||||||
|
data-testid="code-text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="py-4">
|
<div className="py-4" data-testid="error">
|
||||||
<Alert>{error}</Alert>
|
<Alert>{error}</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -152,6 +153,7 @@ export function VerifyForm({
|
|||||||
variant={ButtonVariants.Primary}
|
variant={ButtonVariants.Primary}
|
||||||
disabled={loading || !formState.isValid}
|
disabled={loading || !formState.isValid}
|
||||||
onClick={handleSubmit(fcn)}
|
onClick={handleSubmit(fcn)}
|
||||||
|
data-testid="submit-button"
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
{t("verify.submit")}
|
{t("verify.submit")}
|
||||||
|
@@ -16,7 +16,8 @@ import {
|
|||||||
getOrgsByDomain,
|
getOrgsByDomain,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
listIDPLinks,
|
listIDPLinks,
|
||||||
listUsers,
|
searchUsers,
|
||||||
|
SearchUsersCommand,
|
||||||
startIdentityProviderFlow,
|
startIdentityProviderFlow,
|
||||||
} from "../zitadel";
|
} from "../zitadel";
|
||||||
import { createSessionAndUpdateCookie } from "./cookie";
|
import { createSessionAndUpdateCookie } from "./cookie";
|
||||||
@@ -25,26 +26,36 @@ export type SendLoginnameCommand = {
|
|||||||
loginName: string;
|
loginName: string;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
suffix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||||
|
|
||||||
export async function sendLoginname(command: SendLoginnameCommand) {
|
export async function sendLoginname(command: SendLoginnameCommand) {
|
||||||
const users = await listUsers({
|
const loginSettingsByContext = await getLoginSettings(command.organization);
|
||||||
loginName: command.loginName,
|
|
||||||
|
if (!loginSettingsByContext) {
|
||||||
|
return { error: "Could not get login settings" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchUsersRequest: SearchUsersCommand = {
|
||||||
|
searchValue: command.loginName,
|
||||||
organizationId: command.organization,
|
organizationId: command.organization,
|
||||||
});
|
loginSettings: loginSettingsByContext,
|
||||||
|
suffix: command.suffix,
|
||||||
|
};
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings(command.organization);
|
const searchResult = await searchUsers(searchUsersRequest);
|
||||||
|
|
||||||
const potentialUsers = users.result.filter((u) => {
|
if ("error" in searchResult && searchResult.error) {
|
||||||
const human = u.type.case === "human" ? u.type.value : undefined;
|
return searchResult;
|
||||||
return loginSettings?.disableLoginWithEmail
|
}
|
||||||
? human?.email?.isVerified && human?.email?.email !== command.loginName
|
|
||||||
: loginSettings?.disableLoginWithPhone
|
if (!("result" in searchResult)) {
|
||||||
? human?.phone?.isVerified && human?.phone?.phone !== command.loginName
|
return { error: "Could not search users" };
|
||||||
: true;
|
}
|
||||||
});
|
|
||||||
|
const { result: potentialUsers } = searchResult;
|
||||||
|
|
||||||
const redirectUserToSingleIDPIfAvailable = async () => {
|
const redirectUserToSingleIDPIfAvailable = async () => {
|
||||||
const identityProviders = await getActiveIdentityProviders(
|
const identityProviders = await getActiveIdentityProviders(
|
||||||
@@ -145,9 +156,50 @@ 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 userId = potentialUsers[0].userId;
|
||||||
|
|
||||||
|
const userLoginSettings = await getLoginSettings(
|
||||||
|
user.details?.resourceOwner,
|
||||||
|
);
|
||||||
|
|
||||||
|
// compare with the concatenated suffix when set
|
||||||
|
const concatLoginname = command.suffix
|
||||||
|
? `${command.loginName}@${command.suffix}`
|
||||||
|
: command.loginName;
|
||||||
|
|
||||||
|
const humanUser =
|
||||||
|
potentialUsers[0].type.case === "human"
|
||||||
|
? potentialUsers[0].type.value
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// recheck login settings after user discovery, as the search might have been done without org scope
|
||||||
|
if (
|
||||||
|
userLoginSettings?.disableLoginWithEmail &&
|
||||||
|
userLoginSettings?.disableLoginWithPhone
|
||||||
|
) {
|
||||||
|
if (user.preferredLoginName !== concatLoginname) {
|
||||||
|
return { error: "User not found in the system!" };
|
||||||
|
}
|
||||||
|
} else if (userLoginSettings?.disableLoginWithEmail) {
|
||||||
|
if (
|
||||||
|
user.preferredLoginName !== concatLoginname ||
|
||||||
|
humanUser?.phone?.phone !== command.loginName
|
||||||
|
) {
|
||||||
|
return { error: "User not found in the system!" };
|
||||||
|
}
|
||||||
|
} else if (userLoginSettings?.disableLoginWithPhone) {
|
||||||
|
if (
|
||||||
|
user.preferredLoginName !== concatLoginname ||
|
||||||
|
humanUser?.email?.email !== command.loginName
|
||||||
|
) {
|
||||||
|
return { error: "User not found in the system!" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
const checks = create(ChecksSchema, {
|
||||||
user: { search: { case: "userId", value: userId } },
|
user: { search: { case: "userId", value: userId } },
|
||||||
});
|
});
|
||||||
@@ -163,7 +215,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check if handling of userstate INITIAL is needed
|
// 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" };
|
return { error: "Initial User not supported" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +225,6 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
|
|
||||||
// this can be expected to be an invite as users created in console have a password set.
|
// this can be expected to be an invite as users created in console have a password set.
|
||||||
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
||||||
const humanUser =
|
|
||||||
potentialUsers[0].type.case === "human"
|
|
||||||
? potentialUsers[0].type.value
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// redirect to /verify invite if no auth method is set and email is not verified
|
// redirect to /verify invite if no auth method is set and email is not verified
|
||||||
const inviteCheck = checkInvite(
|
const inviteCheck = checkInvite(
|
||||||
session,
|
session,
|
||||||
@@ -213,7 +260,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
const method = methods.authMethodTypes[0];
|
const method = methods.authMethodTypes[0];
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
|
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
|
||||||
if (!loginSettings?.allowUsernamePassword) {
|
if (!userLoginSettings?.allowUsernamePassword) {
|
||||||
return {
|
return {
|
||||||
error:
|
error:
|
||||||
"Username Password not allowed! Contact your administrator for more information.",
|
"Username Password not allowed! Contact your administrator for more information.",
|
||||||
@@ -240,7 +287,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||||
if (loginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) {
|
if (userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) {
|
||||||
return {
|
return {
|
||||||
error:
|
error:
|
||||||
"Passkeys not allowed! Contact your administrator for more information.",
|
"Passkeys not allowed! Contact your administrator for more information.",
|
||||||
@@ -303,22 +350,24 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// user not found, check if register is enabled on organization
|
// user not found, check if register is enabled on instance / organization context
|
||||||
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) {
|
if (
|
||||||
// TODO: do we need to handle login hints for IDPs here?
|
loginSettingsByContext?.allowRegister &&
|
||||||
|
!loginSettingsByContext?.allowUsernamePassword
|
||||||
|
) {
|
||||||
const resp = await redirectUserToSingleIDPIfAvailable();
|
const resp = await redirectUserToSingleIDPIfAvailable();
|
||||||
if (resp) {
|
if (resp) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
return { error: "User not found in the system" };
|
return { error: "User not found in the system" };
|
||||||
} else if (
|
} else if (
|
||||||
loginSettings?.allowRegister &&
|
loginSettingsByContext?.allowRegister &&
|
||||||
loginSettings?.allowUsernamePassword
|
loginSettingsByContext?.allowUsernamePassword
|
||||||
) {
|
) {
|
||||||
let orgToRegisterOn: string | undefined = command.organization;
|
let orgToRegisterOn: string | undefined = command.organization;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!loginSettings?.ignoreUnknownUsernames &&
|
!loginSettingsByContext?.ignoreUnknownUsernames &&
|
||||||
!orgToRegisterOn &&
|
!orgToRegisterOn &&
|
||||||
command.loginName &&
|
command.loginName &&
|
||||||
ORG_SUFFIX_REGEX.test(command.loginName)
|
ORG_SUFFIX_REGEX.test(command.loginName)
|
||||||
@@ -338,7 +387,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not register user if ignoreUnknownUsernames is set
|
// do not register user if ignoreUnknownUsernames is set
|
||||||
if (orgToRegisterOn && !loginSettings?.ignoreUnknownUsernames) {
|
if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) {
|
||||||
const params = new URLSearchParams({ organization: orgToRegisterOn });
|
const params = new URLSearchParams({ organization: orgToRegisterOn });
|
||||||
|
|
||||||
if (command.authRequestId) {
|
if (command.authRequestId) {
|
||||||
@@ -353,7 +402,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginSettings?.ignoreUnknownUsernames) {
|
if (loginSettingsByContext?.ignoreUnknownUsernames) {
|
||||||
const paramsPasswordDefault = new URLSearchParams({
|
const paramsPasswordDefault = new URLSearchParams({
|
||||||
loginName: command.loginName,
|
loginName: command.loginName,
|
||||||
});
|
});
|
||||||
|
@@ -26,6 +26,7 @@ import { create, Duration } from "@zitadel/client";
|
|||||||
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
|
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
|
||||||
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||||
import {
|
import {
|
||||||
@@ -324,19 +325,24 @@ export async function createInviteCode(userId: string, host: string | null) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listUsers({
|
export type ListUsersCommand = {
|
||||||
loginName,
|
|
||||||
userName,
|
|
||||||
email,
|
|
||||||
organizationId,
|
|
||||||
}: {
|
|
||||||
loginName?: string;
|
loginName?: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
}) {
|
};
|
||||||
|
|
||||||
|
export async function listUsers({
|
||||||
|
loginName,
|
||||||
|
userName,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
organizationId,
|
||||||
|
}: ListUsersCommand) {
|
||||||
const queries: SearchQuery[] = [];
|
const queries: SearchQuery[] = [];
|
||||||
|
|
||||||
|
// either use loginName or userName, email, phone
|
||||||
if (loginName) {
|
if (loginName) {
|
||||||
queries.push(
|
queries.push(
|
||||||
create(SearchQuerySchema, {
|
create(SearchQuerySchema, {
|
||||||
@@ -349,11 +355,11 @@ export async function listUsers({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
} else if (userName || email || phone) {
|
||||||
|
const orQueries: SearchQuery[] = [];
|
||||||
|
|
||||||
if (userName) {
|
if (userName) {
|
||||||
queries.push(
|
const userNameQuery = create(SearchQuerySchema, {
|
||||||
create(SearchQuerySchema, {
|
|
||||||
query: {
|
query: {
|
||||||
case: "userNameQuery",
|
case: "userNameQuery",
|
||||||
value: {
|
value: {
|
||||||
@@ -361,6 +367,44 @@ export async function listUsers({
|
|||||||
method: TextQueryMethod.EQUALS,
|
method: TextQueryMethod.EQUALS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
orQueries.push(userNameQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
const emailQuery = create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "emailQuery",
|
||||||
|
value: {
|
||||||
|
emailAddress: email,
|
||||||
|
method: TextQueryMethod.EQUALS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
orQueries.push(emailQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phone) {
|
||||||
|
const phoneQuery = create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "phoneQuery",
|
||||||
|
value: {
|
||||||
|
number: phone,
|
||||||
|
method: TextQueryMethod.EQUALS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
orQueries.push(phoneQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
queries.push(
|
||||||
|
create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "orQuery",
|
||||||
|
value: {
|
||||||
|
queries: orQueries,
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -378,20 +422,165 @@ export async function listUsers({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email) {
|
return userService.listUsers({ queries: queries });
|
||||||
queries.push(
|
}
|
||||||
|
|
||||||
|
export type SearchUsersCommand = {
|
||||||
|
searchValue: string;
|
||||||
|
loginSettings: LoginSettings;
|
||||||
|
organizationId?: string;
|
||||||
|
suffix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PhoneQuery = (searchValue: string) =>
|
||||||
|
create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "phoneQuery",
|
||||||
|
value: {
|
||||||
|
number: searchValue,
|
||||||
|
method: TextQueryMethod.EQUALS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoginNameQuery = (searchValue: string) =>
|
||||||
|
create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "loginNameQuery",
|
||||||
|
value: {
|
||||||
|
loginName: searchValue,
|
||||||
|
method: TextQueryMethod.EQUALS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const EmailQuery = (searchValue: string) =>
|
||||||
create(SearchQuerySchema, {
|
create(SearchQuerySchema, {
|
||||||
query: {
|
query: {
|
||||||
case: "emailQuery",
|
case: "emailQuery",
|
||||||
value: {
|
value: {
|
||||||
emailAddress: email,
|
emailAddress: searchValue,
|
||||||
|
method: TextQueryMethod.EQUALS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this is a dedicated search function to search for users from the loginname page
|
||||||
|
* it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found
|
||||||
|
* */
|
||||||
|
|
||||||
|
export async function searchUsers({
|
||||||
|
searchValue,
|
||||||
|
loginSettings,
|
||||||
|
organizationId,
|
||||||
|
suffix,
|
||||||
|
}: SearchUsersCommand) {
|
||||||
|
const queries: SearchQuery[] = [];
|
||||||
|
|
||||||
|
// if a suffix is provided, we search for the userName concatenated with the suffix
|
||||||
|
if (suffix) {
|
||||||
|
const searchValueWithSuffix = `${searchValue}@${suffix}`;
|
||||||
|
const loginNameQuery = LoginNameQuery(searchValueWithSuffix);
|
||||||
|
queries.push(loginNameQuery);
|
||||||
|
} else {
|
||||||
|
const loginNameQuery = LoginNameQuery(searchValue);
|
||||||
|
queries.push(loginNameQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
queries.push(
|
||||||
|
create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "organizationIdQuery",
|
||||||
|
value: {
|
||||||
|
organizationId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return userService.listUsers({ queries: queries });
|
const loginNameResult = await userService.listUsers({ queries: queries });
|
||||||
|
|
||||||
|
if (!loginNameResult || !loginNameResult.details) {
|
||||||
|
return { error: "An error occurred." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginNameResult.result.length > 1) {
|
||||||
|
return { error: "Multiple users found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginNameResult.result.length == 1) {
|
||||||
|
return loginNameResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailAndPhoneQueries: SearchQuery[] = [];
|
||||||
|
if (
|
||||||
|
loginSettings.disableLoginWithEmail &&
|
||||||
|
loginSettings.disableLoginWithPhone
|
||||||
|
) {
|
||||||
|
return { error: "User not found in the system" };
|
||||||
|
} else if (loginSettings.disableLoginWithEmail && searchValue.length <= 20) {
|
||||||
|
const phoneQuery = PhoneQuery(searchValue);
|
||||||
|
emailAndPhoneQueries.push(phoneQuery);
|
||||||
|
} else if (loginSettings.disableLoginWithPhone) {
|
||||||
|
const emailQuery = EmailQuery(searchValue);
|
||||||
|
emailAndPhoneQueries.push(emailQuery);
|
||||||
|
} else {
|
||||||
|
const emailAndPhoneOrQueries: SearchQuery[] = [];
|
||||||
|
|
||||||
|
const emailQuery = EmailQuery(searchValue);
|
||||||
|
emailAndPhoneOrQueries.push(emailQuery);
|
||||||
|
|
||||||
|
let phoneQuery;
|
||||||
|
if (searchValue.length <= 20) {
|
||||||
|
phoneQuery = PhoneQuery(searchValue);
|
||||||
|
emailAndPhoneOrQueries.push(phoneQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
emailAndPhoneQueries.push(
|
||||||
|
create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "orQuery",
|
||||||
|
value: {
|
||||||
|
queries: emailAndPhoneOrQueries,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
queries.push(
|
||||||
|
create(SearchQuerySchema, {
|
||||||
|
query: {
|
||||||
|
case: "organizationIdQuery",
|
||||||
|
value: {
|
||||||
|
organizationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailOrPhoneResult = await userService.listUsers({
|
||||||
|
queries: emailAndPhoneQueries,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!emailOrPhoneResult || !emailOrPhoneResult.details) {
|
||||||
|
return { error: "An error occurred." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailOrPhoneResult.result.length > 1) {
|
||||||
|
return { error: "Multiple users found." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailOrPhoneResult.result.length == 1) {
|
||||||
|
return loginNameResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: "User not found in the system" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDefaultOrg(): Promise<Organization | null> {
|
export async function getDefaultOrg(): Promise<Organization | null> {
|
||||||
|
Reference in New Issue
Block a user