chore: reproducible pipeline with dev containers (#10305)

# Which Problems Are Solved

- The previous monorepo in monorepo structure for the login app and its
related packages was fragmented, complicated and buggy.
- The process for building and testing the login container was
inconsistent between local development and CI.
- Lack of clear documentation as well as easy and reliable ways for
non-frontend developers to reproduce and fix failing PR checks locally.

# How the Problems Are Solved

- Consolidated the login app and its related npm packages by moving the
main package to `apps/login/apps/login` and merging
`apps/login/packages/integration` and `apps/login/packages/acceptance`
into the main `apps/login` package.
- Migrated from Docker Compose-based test setups to dev container-based
setups, adding support for multiple dev container configurations:
  - `.devcontainer/base`
  - `.devcontainer/turbo-lint-unit`
  - `.devcontainer/turbo-lint-unit-debug`
  - `.devcontainer/login-integration`
  - `.devcontainer/login-integration-debug`
- Added npm scripts to run the new dev container setups, enabling exact
reproduction of GitHub PR checks locally, and updated the pipeline to
use these containers.
- Cleaned up Dockerfiles and docker-bake.hcl files to only build the
production image for the login app.
- Cleaned up compose files to focus on dev environments in dev
containers.
- Updated `CONTRIBUTING.md` with guidance on running and debugging PR
checks locally using the new dev container approach.
- Introduced separate Dockerfiles for the login app to distinguish
between using published client packages and building clients from local
protos.
- Ensured the login container is always built in the pipeline for use in
integration and acceptance tests.
- Updated Makefile and GitHub Actions workflows to use
`--frozen-lockfile` for installing pnpm packages, ensuring reproducible
installs.
- Disabled GitHub release creation by the changeset action.
- Refactored the `/build` directory structure for clarity and
maintainability.
- Added a `clean` command to `docks/package.json`.
- Experimentally added `knip` to the `zitadel-client` package for
improved linting of dependencies and exports.

# Additional Changes

- Fixed Makefile commands for consistency and reliability.
- Improved the structure and clarity of the `/build` directory to
support seamless integration of the login build.
- Enhanced documentation and developer experience for running and
debugging CI checks locally.

# Additional Context

- See updated `CONTRIBUTING.md` for new local development and debugging
instructions.
- These changes are a prerequisite for further improvements to the CI
pipeline and local development workflow.
- Closes #10276
This commit is contained in:
Elio Bischof
2025-07-24 14:22:32 +02:00
committed by GitHub
parent af66c9844a
commit b10455b51f
430 changed files with 2869 additions and 4108 deletions

View File

@@ -0,0 +1,7 @@
import { test } from "@playwright/test";
import { loginScreenExpect, loginWithPassword } from "./login";
test("admin login", async ({ page }) => {
await loginWithPassword(page, process.env["ZITADEL_ADMIN_USER"], "Password1!");
await loginScreenExpect(page, "ZITADEL Admin");
});

View File

@@ -0,0 +1,12 @@
import { expect, Page } from "@playwright/test";
const codeTextInput = "code-text-input";
export async function codeScreen(page: Page, code: string) {
await page.getByTestId(codeTextInput).pressSequentially(code);
}
export async function codeScreenExpect(page: Page, code: string) {
await expect(page.getByTestId(codeTextInput)).toHaveValue(code);
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify OTP code");
}

View File

@@ -0,0 +1,17 @@
import { Page } from "@playwright/test";
import { codeScreen } from "./code-screen";
import { getOtpFromSink } from "./sink";
export async function otpFromSink(page: Page, key: string) {
const c = await getOtpFromSink(key);
await code(page, c);
}
export async function code(page: Page, code: string) {
await codeScreen(page, code);
await page.getByTestId("submit-button").click();
}
export async function codeResend(page: Page) {
await page.getByTestId("resend-button").click();
}

View 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");
}

View File

@@ -0,0 +1,69 @@
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, "../../login/.env.test.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());
const c = await getCodeFromSink(user.getUsername());
await emailVerify(page, c);
// wait for resend of the code
await page.waitForTimeout(2000);
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);
const c = await getCodeFromSink(user.getUsername());
// wait for resend of the code
await page.waitForTimeout(2000);
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());
const c = await getCodeFromSink(user.getUsername());
await emailVerifyResend(page);
// wait for resend of the code
await page.waitForTimeout(2000);
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);
});

View 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();
}

View File

@@ -0,0 +1,102 @@
// Note for all tests, in case Apple doesn't deliver all relevant information per default
// We should add an action in the needed cases
import test from "@playwright/test";
test("login with Apple IDP", async ({ page }) => {
test.skip();
// Given an Apple IDP is configured on the organization
// Given the user has an Apple added as auth method
// User authenticates with Apple
// User is redirected back to login
// User is redirected to the app
});
test("login with Apple IDP - error", async ({ page }) => {
test.skip();
// Given an Apple IDP is configured on the organization
// Given the user has an Apple added as auth method
// User is redirected to Apple
// User authenticates with Apple and gets an error
// User is redirect back to login
// An error is shown to the user "Something went wrong in Apple Login"
});
test("login with Apple IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Apple is configure on the organization as only authencation method
// Given idp Apple is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Apple
// User authenticates in Apple
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Apple IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Apple is configure on the organization as only authencation method
// Given idp Apple is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Apple
// User authenticates in Apple
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Apple IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Apple is configure on the organization as only authencation method
// Given idp Apple is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Apple
// User authenticates in Apple
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Apple IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Apple is configure on the organization as only authencation method
// Given idp Apple is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Apple
// User authenticates in Apple with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Apple IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Apple is configure on the organization as only authencation method
// Given idp Apple is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Apple
// User authenticates in Apple with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Apple IDP, no user linked, user link successful", async ({ page }) => {
test.skip();
// Given idp Apple is configure on the organization as only authencation method
// Given idp Apple is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Apple
// User authenticates in Apple with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,99 @@
import test from "@playwright/test";
test("login with Generic JWT IDP", async ({ page }) => {
test.skip();
// Given a Generic JWT IDP is configured on the organization
// Given the user has Generic JWT IDP added as auth method
// User authenticates with the Generic JWT IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with Generic JWT IDP - error", async ({ page }) => {
test.skip();
// Given the Generic JWT IDP is configured on the organization
// Given the user has Generic JWT IDP added as auth method
// User is redirected to the Generic JWT IDP
// User authenticates with the Generic JWT IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Generic JWT IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Generic JWT is configure on the organization as only authencation method
// Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic JWT
// User authenticates in Generic JWT
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic JWT IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Generic JWT is configure on the organization as only authencation method
// Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic JWT
// User authenticates in Generic JWT
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic JWT IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Generic JWT is configure on the organization as only authencation method
// Given idp Generic JWT is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic JWT
// User authenticates in Generic JWT
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Generic JWT IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Generic JWT is configure on the organization as only authencation method
// Given idp Generic JWT is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Generic JWT
// User authenticates in Generic JWT with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic JWT IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Generic JWT is configure on the organization as only authencation method
// Given idp Generic JWT is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Generic JWT
// User authenticates in Generic JWT with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Generic JWT IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Generic JWT is configure on the organization as only authencation method
// Given idp Generic JWT is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Generic JWT
// User authenticates in Generic JWT with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,99 @@
import test from "@playwright/test";
test("login with Generic OAuth IDP", async ({ page }) => {
test.skip();
// Given a Generic OAuth IDP is configured on the organization
// Given the user has Generic OAuth IDP added as auth method
// User authenticates with the Generic OAuth IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with Generic OAuth IDP - error", async ({ page }) => {
test.skip();
// Given the Generic OAuth IDP is configured on the organization
// Given the user has Generic OAuth IDP added as auth method
// User is redirected to the Generic OAuth IDP
// User authenticates with the Generic OAuth IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Generic OAuth IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Generic OAuth is configure on the organization as only authencation method
// Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic OAuth
// User authenticates in Generic OAuth
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic OAuth IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Generic OAuth is configure on the organization as only authencation method
// Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic OAuth
// User authenticates in Generic OAuth
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic OAuth IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Generic OAuth is configure on the organization as only authencation method
// Given idp Generic OAuth is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic OAuth
// User authenticates in Generic OAuth
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Generic OAuth IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Generic OAuth is configure on the organization as only authencation method
// Given idp Generic OAuth is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Generic OAuth
// User authenticates in Generic OAuth with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic OAuth IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Generic OAuth is configure on the organization as only authencation method
// Given idp Generic OAuth is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Generic OAuth
// User authenticates in Generic OAuth with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Generic OAuth IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Generic OAuth is configure on the organization as only authencation method
// Given idp Generic OAuth is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Generic OAuth
// User authenticates in Generic OAuth with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,101 @@
// Note, we should use a provider such as Google to test this, where we know OIDC standard is properly implemented
import test from "@playwright/test";
test("login with Generic OIDC IDP", async ({ page }) => {
test.skip();
// Given a Generic OIDC IDP is configured on the organization
// Given the user has Generic OIDC IDP added as auth method
// User authenticates with the Generic OIDC IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with Generic OIDC IDP - error", async ({ page }) => {
test.skip();
// Given the Generic OIDC IDP is configured on the organization
// Given the user has Generic OIDC IDP added as auth method
// User is redirected to the Generic OIDC IDP
// User authenticates with the Generic OIDC IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Generic OIDC IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Generic OIDC is configure on the organization as only authencation method
// Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic OIDC
// User authenticates in Generic OIDC
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic OIDC IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Generic OIDC is configure on the organization as only authencation method
// Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic OIDC
// User authenticates in Generic OIDC
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic OIDC IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Generic OIDC is configure on the organization as only authencation method
// Given idp Generic OIDC is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Generic OIDC
// User authenticates in Generic OIDC
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Generic OIDC IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Generic OIDC is configure on the organization as only authencation method
// Given idp Generic OIDC is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Generic OIDC
// User authenticates in Generic OIDC with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Generic OIDC IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Generic OIDC is configure on the organization as only authencation method
// Given idp Generic OIDC is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Generic OIDC
// User authenticates in Generic OIDC with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Generic OIDC IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Generic OIDC is configure on the organization as only authencation method
// Given idp Generic OIDC is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Generic OIDC
// User authenticates in Generic OIDC with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,103 @@
import test from "@playwright/test";
test("login with GitHub Enterprise IDP", async ({ page }) => {
test.skip();
// Given a GitHub Enterprise IDP is configured on the organization
// Given the user has GitHub Enterprise IDP added as auth method
// User authenticates with the GitHub Enterprise IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with GitHub Enterprise IDP - error", async ({ page }) => {
test.skip();
// Given the GitHub Enterprise IDP is configured on the organization
// Given the user has GitHub Enterprise IDP added as auth method
// User is redirected to the GitHub Enterprise IDP
// User authenticates with the GitHub Enterprise IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with GitHub Enterprise IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp GitHub Enterprise is configure on the organization as only authencation method
// Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled
// Given ZITADEL Action is added to autofill missing user information
// Given no user exists yet
// User is automatically redirected to GitHub Enterprise
// User authenticates in GitHub Enterprise
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with GitHub Enterprise IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp GitHub Enterprise is configure on the organization as only authencation method
// Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to GitHub Enterprise
// User authenticates in GitHub Enterprise
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with GitHub Enterprise IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp GitHub Enterprise is configure on the organization as only authencation method
// Given idp GitHub Enterprise is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to GitHub Enterprise
// User authenticates in GitHub Enterprise
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with GitHub Enterprise IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp GitHub Enterprise is configure on the organization as only authencation method
// Given idp GitHub Enterprise is configure with account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com exists
// User is automatically redirected to GitHub Enterprise
// User authenticates in GitHub Enterprise with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with GitHub Enterprise IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp GitHub Enterprise is configure on the organization as only authencation method
// Given idp GitHub Enterprise is configure with manually account linking not allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to GitHub Enterprise
// User authenticates in GitHub Enterprise with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with GitHub Enterprise IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp GitHub Enterprise is configure on the organization as only authencation method
// Given idp GitHub Enterprise is configure with manually account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to GitHub Enterprise
// User authenticates in GitHub Enterprise with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,103 @@
import test from "@playwright/test";
test("login with GitHub IDP", async ({ page }) => {
test.skip();
// Given a GitHub IDP is configured on the organization
// Given the user has GitHub IDP added as auth method
// User authenticates with the GitHub IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with GitHub IDP - error", async ({ page }) => {
test.skip();
// Given the GitHub IDP is configured on the organization
// Given the user has GitHub IDP added as auth method
// User is redirected to the GitHub IDP
// User authenticates with the GitHub IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with GitHub IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp GitHub is configure on the organization as only authencation method
// Given idp GitHub is configure with account creation alloweed, and automatic creation enabled
// Given ZITADEL Action is added to autofill missing user information
// Given no user exists yet
// User is automatically redirected to GitHub
// User authenticates in GitHub
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with GitHub IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp GitHub is configure on the organization as only authencation method
// Given idp GitHub is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to GitHub
// User authenticates in GitHub
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with GitHub IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp GitHub is configure on the organization as only authencation method
// Given idp GitHub is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to GitHub
// User authenticates in GitHub
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with GitHub IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp GitHub is configure on the organization as only authencation method
// Given idp GitHub is configure with account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com exists
// User is automatically redirected to GitHub
// User authenticates in GitHub with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with GitHub IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp GitHub is configure on the organization as only authencation method
// Given idp GitHub is configure with manually account linking not allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to GitHub
// User authenticates in GitHub with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with GitHub IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp GitHub is configure on the organization as only authencation method
// Given idp GitHub is configure with manually account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to GitHub
// User authenticates in GitHub with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,103 @@
import test from "@playwright/test";
test("login with GitLab Self-Hosted IDP", async ({ page }) => {
test.skip();
// Given a GitLab Self-Hosted IDP is configured on the organization
// Given the user has GitLab Self-Hosted IDP added as auth method
// User authenticates with the GitLab Self-Hosted IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with GitLab Self-Hosted IDP - error", async ({ page }) => {
test.skip();
// Given the GitLab Self-Hosted IDP is configured on the organization
// Given the user has GitLab Self-Hosted IDP added as auth method
// User is redirected to the GitLab Self-Hosted IDP
// User authenticates with the GitLab Self-Hosted IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Gitlab Self-Hosted IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
// Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled
// Given ZITADEL Action is added to autofill missing user information
// Given no user exists yet
// User is automatically redirected to Gitlab Self-Hosted
// User authenticates in Gitlab Self-Hosted
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Gitlab Self-Hosted IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
// Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Gitlab Self-Hosted
// User authenticates in Gitlab Self-Hosted
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Gitlab Self-Hosted IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
// Given idp Gitlab Self-Hosted is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Gitlab Self-Hosted
// User authenticates in Gitlab Self-Hosted
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Gitlab Self-Hosted IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
// Given idp Gitlab Self-Hosted is configure with account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Gitlab Self-Hosted
// User authenticates in Gitlab Self-Hosted with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Gitlab Self-Hosted IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
// Given idp Gitlab Self-Hosted is configure with manually account linking not allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Gitlab Self-Hosted
// User authenticates in Gitlab Self-Hosted with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Gitlab Self-Hosted IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
// Given idp Gitlab Self-Hosted is configure with manually account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Gitlab Self-Hosted
// User authenticates in Gitlab Self-Hosted with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,103 @@
import test from "@playwright/test";
test("login with GitLab IDP", async ({ page }) => {
test.skip();
// Given a GitLab IDP is configured on the organization
// Given the user has GitLab IDP added as auth method
// User authenticates with the GitLab IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with GitLab IDP - error", async ({ page }) => {
test.skip();
// Given the GitLab IDP is configured on the organization
// Given the user has GitLab IDP added as auth method
// User is redirected to the GitLab IDP
// User authenticates with the GitLab IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Gitlab IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Gitlab is configure on the organization as only authencation method
// Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled
// Given ZITADEL Action is added to autofill missing user information
// Given no user exists yet
// User is automatically redirected to Gitlab
// User authenticates in Gitlab
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Gitlab IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Gitlab is configure on the organization as only authencation method
// Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Gitlab
// User authenticates in Gitlab
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Gitlab IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Gitlab is configure on the organization as only authencation method
// Given idp Gitlab is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Gitlab
// User authenticates in Gitlab
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Gitlab IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Gitlab is configure on the organization as only authencation method
// Given idp Gitlab is configure with account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Gitlab
// User authenticates in Gitlab with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Gitlab IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Gitlab is configure on the organization as only authencation method
// Given idp Gitlab is configure with manually account linking not allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Gitlab
// User authenticates in Gitlab with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Gitlab IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Gitlab is configure on the organization as only authencation method
// Given idp Gitlab is configure with manually account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Gitlab
// User authenticates in Gitlab with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,99 @@
import test from "@playwright/test";
test("login with Google IDP", async ({ page }) => {
test.skip();
// Given a Google IDP is configured on the organization
// Given the user has Google IDP added as auth method
// User authenticates with the Google IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with Google IDP - error", async ({ page }) => {
test.skip();
// Given the Google IDP is configured on the organization
// Given the user has Google IDP added as auth method
// User is redirected to the Google IDP
// User authenticates with the Google IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Google IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Google is configure on the organization as only authencation method
// Given idp Google is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Google
// User authenticates in Google
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Google IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Google is configure on the organization as only authencation method
// Given idp Google is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Google
// User authenticates in Google
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Google IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Google is configure on the organization as only authencation method
// Given idp Google is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Google
// User authenticates in Google
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Google IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Google is configure on the organization as only authencation method
// Given idp Google is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Google
// User authenticates in Google with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Google IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Google is configure on the organization as only authencation method
// Given idp Google is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Google
// User authenticates in Google with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Google IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Google is configure on the organization as only authencation method
// Given idp Google is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Google
// User authenticates in Google with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,99 @@
import test from "@playwright/test";
test("login with LDAP IDP", async ({ page }) => {
test.skip();
// Given a LDAP IDP is configured on the organization
// Given the user has LDAP IDP added as auth method
// User authenticates with the LDAP IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with LDAP IDP - error", async ({ page }) => {
test.skip();
// Given the LDAP IDP is configured on the organization
// Given the user has LDAP IDP added as auth method
// User is redirected to the LDAP IDP
// User authenticates with the LDAP IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with LDAP IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp LDAP is configure on the organization as only authencation method
// Given idp LDAP is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to LDAP
// User authenticates in LDAP
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with LDAP IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp LDAP is configure on the organization as only authencation method
// Given idp LDAP is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to LDAP
// User authenticates in LDAP
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with LDAP IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp LDAP is configure on the organization as only authencation method
// Given idp LDAP is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to LDAP
// User authenticates in LDAP
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with LDAP IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp LDAP is configure on the organization as only authencation method
// Given idp LDAP is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to LDAP
// User authenticates in LDAP with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with LDAP IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp LDAP is configure on the organization as only authencation method
// Given idp LDAP is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to LDAP
// User authenticates in LDAP with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with LDAP IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp LDAP is configure on the organization as only authencation method
// Given idp LDAP is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to LDAP
// User authenticates in LDAP with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,102 @@
// Note for all tests, in case Microsoft doesn't deliver all relevant information per default
// We should add an action in the needed cases
import test from "@playwright/test";
test("login with Microsoft IDP", async ({ page }) => {
test.skip();
// Given a Microsoft IDP is configured on the organization
// Given the user has Microsoft IDP added as auth method
// User authenticates with the Microsoft IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with Microsoft IDP - error", async ({ page }) => {
test.skip();
// Given the Microsoft IDP is configured on the organization
// Given the user has Microsoft IDP added as auth method
// User is redirected to the Microsoft IDP
// User authenticates with the Microsoft IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with Microsoft IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp Microsoft is configure on the organization as only authencation method
// Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Microsoft
// User authenticates in Microsoft
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Microsoft IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp Microsoft is configure on the organization as only authencation method
// Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Microsoft
// User authenticates in Microsoft
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Microsoft IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp Microsoft is configure on the organization as only authencation method
// Given idp Microsoft is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to Microsoft
// User authenticates in Microsoft
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with Microsoft IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp Microsoft is configure on the organization as only authencation method
// Given idp Microsoft is configure with account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com exists
// User is automatically redirected to Microsoft
// User authenticates in Microsoft with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with Microsoft IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp Microsoft is configure on the organization as only authencation method
// Given idp Microsoft is configure with manually account linking not allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Microsoft
// User authenticates in Microsoft with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with Microsoft IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp Microsoft is configure on the organization as only authencation method
// Given idp Microsoft is configure with manually account linking allowed, and linking set to existing email
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to Microsoft
// User authenticates in Microsoft with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,103 @@
import test from "@playwright/test";
test("login with SAML IDP", async ({ page }) => {
test.skip();
// Given a SAML IDP is configured on the organization
// Given the user has SAML IDP added as auth method
// User authenticates with the SAML IDP
// User is redirected back to login
// User is redirected to the app
});
test("login with SAML IDP - error", async ({ page }) => {
test.skip();
// Given the SAML IDP is configured on the organization
// Given the user has SAML IDP added as auth method
// User is redirected to the SAML IDP
// User authenticates with the SAML IDP and gets an error
// User is redirected back to login
// An error is shown to the user "Something went wrong"
});
test("login with SAML IDP, no user existing - auto register", async ({ page }) => {
test.skip();
// Given idp SAML is configure on the organization as only authencation method
// Given idp SAML is configure with account creation alloweed, and automatic creation enabled
// Given ZITADEL Action is added to autofill missing user information
// Given no user exists yet
// User is automatically redirected to SAML
// User authenticates in SAML
// User is redirect to ZITADEL login
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with SAML IDP, no user existing - auto register not possible", async ({ page }) => {
test.skip();
// Given idp SAML is configure on the organization as only authencation method
// Given idp SAML is configure with account creation alloweed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to SAML
// User authenticates in SAML
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// User will see the registration page with pre filled user information
// User fills missing information
// User clicks register button
// User is created in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with SAML IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
page,
}) => {
test.skip();
// Given idp SAML is configure on the organization as only authencation method
// Given idp SAML is configure with account creation not allowed, and automatic creation enabled
// Given no user exists yet
// User is automatically redirected to SAML
// User authenticates in SAML
// User is redirect to ZITADEL login
// Because of missing informaiton on the user auto creation is not possible
// Error message is shown, that registration of the user was not possible due to missing information
});
test("login with SAML IDP, no user linked - auto link", async ({ page }) => {
test.skip();
// Given idp SAML is configure on the organization as only authencation method
// Given idp SAML is configure with account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com exists
// User is automatically redirected to SAML
// User authenticates in SAML with user@zitadel.com
// User is redirect to ZITADEL login
// User is linked with existing user in ZITADEL
// User is redirected to the app (default redirect url)
});
test("login with SAML IDP, no user linked, linking not possible", async ({ page }) => {
test.skip();
// Given idp SAML is configure on the organization as only authencation method
// Given idp SAML is configure with manually account linking not allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to SAML
// User authenticates in SAML with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User will get an error message that account linking wasn't possible
});
test("login with SAML IDP, no user linked, linking successful", async ({ page }) => {
test.skip();
// Given idp SAML is configure on the organization as only authencation method
// Given idp SAML is configure with manually account linking allowed, and linking set to existing email
// Given ZITADEL Action is added to autofill missing user information
// Given user with email address user@zitadel.com doesn't exists
// User is automatically redirected to SAML
// User authenticates in SAML with user@zitadel.com
// User is redirect to ZITADEL login
// User with email address user@zitadel.com can not be found
// User is prompted to link the account manually
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,57 @@
import test from "@playwright/test";
test("login with mfa setup, mfa setup prompt", async ({ page }) => {
test.skip();
// Given the organization has enabled at least one mfa types
// Given the user has a password but no mfa registered
// User authenticates with login name and password
// User is prompted to setup a mfa, mfa providers are listed, the user can choose the provider
});
test("login with mfa setup, no mfa setup prompt", async ({ page }) => {
test.skip();
// Given the organization has set "multifactor init check time" to 0
// Given the organization has enabled mfa types
// Given the user has a password but no mfa registered
// User authenticates with loginname and password
// user is directly loged in and not prompted to setup mfa
});
test("login with mfa setup, force mfa for local authenticated users", async ({ page }) => {
test.skip();
// Given the organization has enabled force mfa for local authentiacted users
// Given the organization has enabled all possible mfa types
// Given the user has a password but no mfa registered
// User authenticates with loginname and password
// User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
});
test("login with mfa setup, force mfa - local user", async ({ page }) => {
test.skip();
// Given the organization has enabled force mfa for local authentiacted users
// Given the organization has enabled all possible mfa types
// Given the user has a password but no mfa registered
// User authenticates with loginname and password
// User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
});
test("login with mfa setup, force mfa - external user", async ({ page }) => {
test.skip();
// Given the organization has enabled force mfa
// Given the organization has enabled all possible mfa types
// Given the user has an idp but no mfa registered
// enter login name
// redirect to configured external idp
// User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
});
test("login with mfa setup, force mfa - local user, wrong password", async ({ page }) => {
test.skip();
// Given the organization has a password lockout policy set to 1 on the max password attempts
// Given the user has only a password as auth methos
// enter login name
// enter wrong password
// User will get an error "Wrong password"
// enter password
// User will get an error "Max password attempts reached - user is locked. Please reach out to your administrator"
});

View File

@@ -0,0 +1,41 @@
import { expect, Page } from "@playwright/test";
import { code, otpFromSink } from "./code";
import { loginname } from "./loginname";
import { password } from "./password";
import { totp } from "./zitadel";
export async function startLogin(page: Page) {
await page.goto(`./loginname`);
}
export async function loginWithPassword(page: Page, username: string, pw: string) {
await startLogin(page);
await loginname(page, username);
await password(page, pw);
}
export async function loginWithPasskey(page: Page, authenticatorId: string, username: string) {
await startLogin(page);
await loginname(page, username);
// await passkey(page, authenticatorId);
}
export async function loginScreenExpect(page: Page, fullName: string) {
await expect(page).toHaveURL(/.*signedin.*/);
await expect(page.getByRole("heading")).toContainText(fullName);
}
export async function loginWithPasswordAndEmailOTP(page: Page, username: string, password: string, email: string) {
await loginWithPassword(page, username, password);
await otpFromSink(page, email);
}
export async function loginWithPasswordAndPhoneOTP(page: Page, username: string, password: string, phone: string) {
await loginWithPassword(page, username, password);
await otpFromSink(page, phone);
}
export async function loginWithPasswordAndTOTP(page: Page, username: string, password: string, secret: string) {
await loginWithPassword(page, username, password);
await code(page, totp(secret));
}

View File

@@ -0,0 +1,12 @@
import { expect, Page } from "@playwright/test";
const usernameTextInput = "username-text-input";
export async function loginnameScreen(page: Page, username: string) {
await page.getByTestId(usernameTextInput).pressSequentially(username);
}
export async function loginnameScreenExpect(page: Page, username: string) {
await expect(page.getByTestId(usernameTextInput)).toHaveValue(username);
await expect(page.getByTestId("error").locator("div")).toContainText("User not found in the system");
}

View File

@@ -0,0 +1,7 @@
import { Page } from "@playwright/test";
import { loginnameScreen } from "./loginname-screen";
export async function loginname(page: Page, username: string) {
await loginnameScreen(page, username);
await page.getByTestId("submit-button").click();
}

View File

@@ -0,0 +1,109 @@
import { expect, Page } from "@playwright/test";
import { CDPSession } from "playwright-core";
interface session {
client: CDPSession;
authenticatorId: string;
}
async function client(page: Page): Promise<session> {
const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send("WebAuthn.enable", { enableUI: false });
const result = await cdpSession.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "internal",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
return { client: cdpSession, authenticatorId: result.authenticatorId };
}
export async function passkeyRegister(page: Page): Promise<string> {
const session = await client(page);
await passkeyNotExisting(session.client, session.authenticatorId);
await simulateSuccessfulPasskeyRegister(session.client, session.authenticatorId, () =>
page.getByTestId("submit-button").click(),
);
await passkeyRegistered(session.client, session.authenticatorId);
return session.authenticatorId;
}
export async function passkey(page: Page, authenticatorId: string) {
const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send("WebAuthn.enable", { enableUI: false });
const signCount = await passkeyExisting(cdpSession, authenticatorId);
await simulateSuccessfulPasskeyInput(cdpSession, authenticatorId, () => page.getByTestId("submit-button").click());
await passkeyUsed(cdpSession, authenticatorId, signCount);
}
async function passkeyNotExisting(client: CDPSession, authenticatorId: string) {
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
expect(result.credentials).toHaveLength(0);
}
async function passkeyRegistered(client: CDPSession, authenticatorId: string) {
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
expect(result.credentials).toHaveLength(1);
await passkeyUsed(client, authenticatorId, 0);
}
async function passkeyExisting(client: CDPSession, authenticatorId: string): Promise<number> {
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
expect(result.credentials).toHaveLength(1);
return result.credentials[0].signCount;
}
async function passkeyUsed(client: CDPSession, authenticatorId: string, signCount: number) {
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0].signCount).toBeGreaterThan(signCount);
}
async function simulateSuccessfulPasskeyRegister(
client: CDPSession,
authenticatorId: string,
operationTrigger: () => Promise<void>,
) {
// initialize event listeners to wait for a successful passkey input event
const operationCompleted = new Promise<void>((resolve) => {
client.on("WebAuthn.credentialAdded", () => {
console.log("Credential Added!");
resolve();
});
});
// perform a user action that triggers passkey prompt
await operationTrigger();
// wait to receive the event that the passkey was successfully registered or verified
await operationCompleted;
}
async function simulateSuccessfulPasskeyInput(
client: CDPSession,
authenticatorId: string,
operationTrigger: () => Promise<void>,
) {
// initialize event listeners to wait for a successful passkey input event
const operationCompleted = new Promise<void>((resolve) => {
client.on("WebAuthn.credentialAsserted", () => {
console.log("Credential Asserted!");
resolve();
});
});
// perform a user action that triggers passkey prompt
await operationTrigger();
// wait to receive the event that the passkey was successfully registered or verified
await operationCompleted;
}

View File

@@ -0,0 +1,98 @@
import { expect, Page } from "@playwright/test";
import { getCodeFromSink } from "./sink";
const codeField = "code-text-input";
const passwordField = "password-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 symbolCheck = "symbol-check";
const numberCheck = "number-check";
const uppercaseCheck = "uppercase-check";
const lowercaseCheck = "lowercase-check";
const equalCheck = "equal-check";
const matchText = "Matches";
const noMatchText = "Doesn't match";
export async function changePasswordScreen(page: Page, password1: string, password2: string) {
await page.getByTestId(passwordChangeField).pressSequentially(password1);
await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2);
}
export async function passwordScreen(page: Page, password: string) {
await page.getByTestId(passwordField).pressSequentially(password);
}
export async function passwordScreenExpect(page: Page, password: string) {
await expect(page.getByTestId(passwordField)).toHaveValue(password);
await expect(page.getByTestId("error").locator("div")).toContainText("Failed to authenticate.");
}
export async function changePasswordScreenExpect(
page: Page,
password1: string,
password2: string,
length: boolean,
symbol: boolean,
number: boolean,
uppercase: boolean,
lowercase: boolean,
equals: boolean,
) {
await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1);
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, symbolCheck, symbol);
await checkContent(page, numberCheck, number);
await checkContent(page, uppercaseCheck, uppercase);
await checkContent(page, lowercaseCheck, lowercase);
await checkContent(page, equalCheck, equals);
}
async function checkContent(page: Page, testid: string, match: boolean) {
if (match) {
await expect(page.getByTestId(testid)).toContainText(matchText);
} else {
await expect(page.getByTestId(testid)).toContainText(noMatchText);
}
}
export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) {
const c = await getCodeFromSink(username);
await page.getByTestId(codeField).pressSequentially(c);
await page.getByTestId(passwordSetField).pressSequentially(password1);
await page.getByTestId(passwordSetConfirmField).pressSequentially(password2);
}
export async function resetPasswordScreenExpect(
page: Page,
password1: string,
password2: string,
length: boolean,
symbol: boolean,
number: boolean,
uppercase: boolean,
lowercase: boolean,
equals: boolean,
) {
await expect(page.getByTestId(passwordSetField)).toHaveValue(password1);
await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2);
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
}

View File

@@ -0,0 +1,29 @@
import { Page } from "@playwright/test";
import { changePasswordScreen, passwordScreen, resetPasswordScreen } from "./password-screen";
const passwordSubmitButton = "submit-button";
const passwordResetButton = "reset-button";
export async function startChangePassword(page: Page, loginname: string) {
await page.goto("./password/change?" + new URLSearchParams({ loginName: loginname }));
}
export async function changePassword(page: Page, password: string) {
await changePasswordScreen(page, password, password);
await page.getByTestId(passwordSubmitButton).click();
}
export async function password(page: Page, password: string) {
await passwordScreen(page, password);
await page.getByTestId(passwordSubmitButton).click();
}
export async function startResetPassword(page: Page) {
await page.getByTestId(passwordResetButton).click();
}
export async function resetPassword(page: Page, username: string, password: string) {
await startResetPassword(page);
await resetPasswordScreen(page, username, password, password);
await page.getByTestId(passwordSubmitButton).click();
}

View File

@@ -0,0 +1,27 @@
import { Page } from "@playwright/test";
const passwordField = "password-text-input";
const passwordConfirmField = "password-confirm-text-input";
export async function registerUserScreenPassword(page: Page, firstname: string, lastname: string, email: string) {
await registerUserScreen(page, firstname, lastname, email);
await page.getByTestId("password-radio").click();
}
export async function registerUserScreenPasskey(page: Page, firstname: string, lastname: string, email: string) {
await registerUserScreen(page, firstname, lastname, email);
await page.getByTestId("passkey-radio").click();
}
export async function registerPasswordScreen(page: Page, password1: string, password2: string) {
await page.getByTestId(passwordField).pressSequentially(password1);
await page.getByTestId(passwordConfirmField).pressSequentially(password2);
}
export async function registerUserScreen(page: Page, firstname: string, lastname: string, email: string) {
await page.getByTestId("firstname-text-input").pressSequentially(firstname);
await page.getByTestId("lastname-text-input").pressSequentially(lastname);
await page.getByTestId("email-text-input").pressSequentially(email);
await page.getByTestId("privacy-policy-checkbox").check();
await page.getByTestId("tos-checkbox").check();
}

View File

@@ -0,0 +1,183 @@
import { faker } from "@faker-js/faker";
import { test } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { loginScreenExpect } from "./login";
import { registerWithPasskey, registerWithPassword } from "./register";
import { removeUserByUsername } from "./zitadel";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
test("register with password", async ({ page }) => {
const username = faker.internet.email();
const password = "Password1!";
const firstname = faker.person.firstName();
const lastname = faker.person.lastName();
await registerWithPassword(page, firstname, lastname, username, password, password);
await loginScreenExpect(page, firstname + " " + lastname);
// wait for projection of user
await page.waitForTimeout(10000);
await removeUserByUsername(username);
});
test("register with passkey", async ({ page }) => {
const username = faker.internet.email();
const firstname = faker.person.firstName();
const lastname = faker.person.lastName();
await registerWithPasskey(page, firstname, lastname, username);
await loginScreenExpect(page, firstname + " " + lastname);
// wait for projection of user
await page.waitForTimeout(10000);
await removeUserByUsername(username);
});
test("register with username and password - only password enabled", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is not enabled
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// Only password is shown as an option - no passkey
// User enters "firstname", "lastname", "username" and "password"
// User is redirected to app (default redirect url)
});
test("register with username and password - wrong password not enough characters", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is not enabled
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// Only password is shown as an option - no passkey
// User enters "firstname", "lastname", "username" and a password thats to short
// Error is shown "Password doesn't match the policy - it must have at least 8 characters"
});
test("register with username and password - wrong password number missing", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is not enabled
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// Only password is shown as an option - no passkey
// User enters "firstname", "lastname", "username" and a password without a number
// Error is shown "Password doesn't match the policy - number missing"
});
test("register with username and password - wrong password upper case missing", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is not enabled
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// Only password is shown as an option - no passkey
// User enters "firstname", "lastname", "username" and a password without an upper case
// Error is shown "Password doesn't match the policy - uppercase letter missing"
});
test("register with username and password - wrong password lower case missing", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is not enabled
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// Only password is shown as an option - no passkey
// User enters "firstname", "lastname", "username" and a password without an lower case
// Error is shown "Password doesn't match the policy - lowercase letter missing"
});
test("register with username and password - wrong password symboo missing", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is not enabled
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// Only password is shown as an option - no passkey
// User enters "firstname", "lastname", "username" and a password without an symbol
// Error is shown "Password doesn't match the policy - symbol missing"
});
test("register with username and password - password and passkey enabled", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is enabled
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// User enters "firstname", "lastname", "username"
// Password and passkey are shown as authentication option
// User clicks password
// User enters password
// User is redirected to app (default redirect url)
});
test("register with username and passkey - password and passkey enabled", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given on the default organization passkey is enabled
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration page
// User enters "firstname", "lastname", "username"
// Password and passkey are shown as authentication option
// User clicks passkey
// Passkey is opened automatically
// User verifies passkey
// User is redirected to app (default redirect url)
});
test("register with username and password - registration disabled", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization no idp is configured and enabled
// Given user doesn't exist
// Button "register new user" is not available
});
test("register with username and password - multiple registration options", async ({ page }) => {
test.skip();
// Given on the default organization "username and password is allowed" is enabled
// Given on the default organization "username registeration allowed" is enabled
// Given on the default organization one idp is configured and enabled
// Given user doesn't exist
// Click on button "register new user"
// User is redirected to registration options
// Local User and idp button are shown
// User clicks idp button
// User enters "firstname", "lastname", "username" and "password"
// User clicks next
// User is redirected to app (default redirect url)
});

View File

@@ -0,0 +1,39 @@
import { Page } from "@playwright/test";
import { emailVerify } from "./email-verify";
import { passkeyRegister } from "./passkey";
import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
import { getCodeFromSink } from "./sink";
export async function registerWithPassword(
page: Page,
firstname: string,
lastname: string,
email: string,
password1: string,
password2: string,
) {
await page.goto("./register");
await registerUserScreenPassword(page, firstname, lastname, email);
await page.getByTestId("submit-button").click();
await registerPasswordScreen(page, password1, password2);
await page.getByTestId("submit-button").click();
await verifyEmail(page, email);
}
export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise<string> {
await page.goto("./register");
await registerUserScreenPasskey(page, firstname, lastname, email);
await page.getByTestId("submit-button").click();
// wait for projection of user
await page.waitForTimeout(10000);
const authId = await passkeyRegister(page);
await verifyEmail(page, email);
return authId;
}
async function verifyEmail(page: Page, email: string) {
const c = await getCodeFromSink(email);
await emailVerify(page, c);
}

View File

@@ -0,0 +1,5 @@
import { Page } from "@playwright/test";
export async function selectNewAccount(page: Page) {
await page.getByRole("link", { name: "Add another account" }).click();
}

View File

@@ -0,0 +1,43 @@
import { Gaxios, GaxiosResponse } from "gaxios";
const awaitNotification = new Gaxios({
url: process.env.SINK_NOTIFICATION_URL,
method: "POST",
retryConfig: {
httpMethodsToRetry: ["POST"],
statusCodesToRetry: [[404, 404]],
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
totalTimeout: 10000, // 10 seconds
onRetryAttempt: (error) => {
console.warn(`Retrying request to sink notification service: ${error.message}`);
},
},
});
export async function getOtpFromSink(recipient: string): Promise<any> {
return awaitNotification.request({ data: { recipient } }).then((response) => {
expectSuccess(response);
const otp = response?.data?.args?.otp;
if (!otp) {
throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
}
return otp;
});
}
export async function getCodeFromSink(recipient: string): Promise<any> {
return awaitNotification.request({ data: { recipient } }).then((response) => {
expectSuccess(response);
const code = response?.data?.args?.code;
if (!code) {
throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`);
}
return code;
});
}
function expectSuccess(response: GaxiosResponse): void {
if (response.status !== 200) {
throw new Error(`Expected HTTP status 200, but got: ${response.status} - ${response.statusText}`);
}
}

View File

@@ -0,0 +1,177 @@
import { Page } from "@playwright/test";
import { registerWithPasskey } from "./register";
import { activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser } from "./zitadel";
export interface userProps {
email: string;
isEmailVerified?: boolean;
firstName: string;
lastName: string;
organization: string;
password: string;
passwordChangeRequired?: boolean;
phone: string;
isPhoneVerified?: boolean;
}
class User {
private readonly props: userProps;
private user: string;
constructor(userProps: userProps) {
this.props = userProps;
}
async ensure(page: Page) {
const response = await addUser(this.props);
this.setUserId(response.userId);
}
async cleanup() {
await removeUser(this.getUserId());
}
public setUserId(userId: string) {
this.user = userId;
}
public getUserId() {
return this.user;
}
public getUsername() {
return this.props.email;
}
public getPassword() {
return this.props.password;
}
public getFirstname() {
return this.props.firstName;
}
public getLastname() {
return this.props.lastName;
}
public getPhone() {
return this.props.phone;
}
public getFullName() {
return `${this.props.firstName} ${this.props.lastName}`;
}
}
export class PasswordUser extends User {
async ensure(page: Page) {
await super.ensure(page);
await eventualNewUser(this.getUserId());
}
}
export enum OtpType {
sms = "sms",
email = "email",
}
export interface otpUserProps {
email: string;
isEmailVerified?: boolean;
firstName: string;
lastName: string;
organization: string;
password: string;
passwordChangeRequired?: boolean;
phone: string;
isPhoneVerified?: boolean;
type: OtpType;
}
export class PasswordUserWithOTP extends User {
private type: OtpType;
constructor(props: otpUserProps) {
super({
email: props.email,
firstName: props.firstName,
lastName: props.lastName,
organization: props.organization,
password: props.password,
phone: props.phone,
isEmailVerified: props.isEmailVerified,
isPhoneVerified: props.isPhoneVerified,
passwordChangeRequired: props.passwordChangeRequired,
});
this.type = props.type;
}
async ensure(page: Page) {
await super.ensure(page);
await activateOTP(this.getUserId(), this.type);
await eventualNewUser(this.getUserId());
}
}
export class PasswordUserWithTOTP extends User {
private secret: string;
async ensure(page: Page) {
await super.ensure(page);
this.secret = await addTOTP(this.getUserId());
await eventualNewUser(this.getUserId());
}
public getSecret(): string {
return this.secret;
}
}
export interface passkeyUserProps {
email: string;
firstName: string;
lastName: string;
organization: string;
phone: string;
isEmailVerified?: boolean;
isPhoneVerified?: boolean;
}
export class PasskeyUser extends User {
private authenticatorId: string;
constructor(props: passkeyUserProps) {
super({
email: props.email,
firstName: props.firstName,
lastName: props.lastName,
organization: props.organization,
password: "",
phone: props.phone,
isEmailVerified: props.isEmailVerified,
isPhoneVerified: props.isPhoneVerified,
});
}
public async ensure(page: Page) {
const authId = await registerWithPasskey(page, this.getFirstname(), this.getLastname(), this.getUsername());
this.authenticatorId = authId;
// wait for projection of user
await page.waitForTimeout(10000);
}
async cleanup() {
const resp: any = await getUserByUsername(this.getUsername());
if (!resp || !resp.result || !resp.result[0]) {
return;
}
await removeUser(resp.result[0].userId);
}
public getAuthenticatorId(): string {
return this.authenticatorId;
}
}

View File

@@ -0,0 +1,43 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { loginScreenExpect, loginWithPasskey } from "./login";
import { PasskeyUser } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasskeyUser }>({
user: async ({ page }, use) => {
const user = new PasskeyUser({
email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number(),
isPhoneVerified: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("username and passkey login", async ({ user, page }) => {
await loginWithPasskey(page, user.getAuthenticatorId(), user.getUsername());
await loginScreenExpect(page, user.getFullName());
});
test("username and passkey login, multiple auth methods", async ({ page }) => {
test.skip();
// Given passkey and password is enabled on the organization of the user
// Given the user has password and passkey registered
// enter username
// passkey popup is directly shown
// user aborts passkey authentication
// user switches to password authentication
// user enters password
// user is redirected to app
});

View 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, "../../login/.env.test.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(10000);
await changePassword(page, changedPw);
await loginScreenExpect(page, user.getFullName());
await loginWithPassword(page, user.getUsername(), changedPw);
await loginScreenExpect(page, user.getFullName());
});

View File

@@ -0,0 +1,54 @@
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, startChangePassword } from "./password";
import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.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: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("username and password changed login", async ({ user, page }) => {
const changedPw = "ChangedPw1!";
await loginWithPassword(page, user.getUsername(), user.getPassword());
// wait for projection of token
await page.waitForTimeout(10000);
await startChangePassword(page, user.getUsername());
await changePassword(page, changedPw);
await loginScreenExpect(page, user.getFullName());
await loginWithPassword(page, user.getUsername(), changedPw);
await loginScreenExpect(page, user.getFullName());
});
test("password change not with desired complexity", async ({ user, page }) => {
const changedPw1 = "change";
const changedPw2 = "chang";
await loginWithPassword(page, user.getUsername(), user.getPassword());
await startChangePassword(page, user.getUsername());
await changePasswordScreen(page, changedPw1, changedPw2);
await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
});

View File

@@ -0,0 +1,98 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { code, codeResend, otpFromSink } from "./code";
import { codeScreenExpect } from "./code-screen";
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } from "./login";
import { OtpType, PasswordUserWithOTP } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => {
const user = new PasswordUserWithOTP({
email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!",
passwordChangeRequired: false,
type: OtpType.email,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test.skip("DOESN'T WORK: username, password and email otp login, enter code manually", async ({ user, page }) => {
// Given email otp is enabled on the organization of the user
// Given the user has only email otp configured as second factor
// User enters username
// User enters password
// User receives an email with a verification code
// User enters the code into the ui
// User is redirected to the app (default redirect url)
await loginWithPasswordAndEmailOTP(page, user.getUsername(), user.getPassword(), user.getUsername());
await loginScreenExpect(page, user.getFullName());
});
test("username, password and email otp login, click link in email", async ({ page }) => {
base.skip();
// Given email otp is enabled on the organization of the user
// Given the user has only email otp configured as second factor
// User enters username
// User enters password
// User receives an email with a verification code
// User clicks link in the email
// User is redirected to the app (default redirect url)
});
test.skip("DOESN'T WORK: username, password and email otp login, resend code", async ({ user, page }) => {
// Given email otp is enabled on the organization of the user
// Given the user has only email otp configured as second factor
// User enters username
// User enters password
// User receives an email with a verification code
// User clicks resend code
// User receives a new email with a verification code
// User enters the new code in the ui
// User is redirected to the app (default redirect url)
await loginWithPassword(page, user.getUsername(), user.getPassword());
await codeResend(page);
await otpFromSink(page, user.getUsername());
await loginScreenExpect(page, user.getFullName());
});
test("username, password and email otp login, wrong code", async ({ user, page }) => {
// Given email otp is enabled on the organization of the user
// Given the user has only email otp configured as second factor
// User enters username
// User enters password
// User receives an email with a verification code
// User enters a wrong code
// Error message - "Invalid code" is shown
const c = "wrongcode";
await loginWithPassword(page, user.getUsername(), user.getPassword());
await code(page, c);
await codeScreenExpect(page, c);
});
test("username, password and email otp login, multiple mfa options", async ({ page }) => {
base.skip();
// Given email otp and sms otp is enabled on the organization of the user
// Given the user has email and sms otp configured as second factor
// User enters username
// User enters password
// User receives an email with a verification code
// User clicks button to use sms otp as second factor
// User receives a sms with a verification code
// User enters code in ui
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,71 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { code } from "./code";
import { codeScreenExpect } from "./code-screen";
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } from "./login";
import { OtpType, PasswordUserWithOTP } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => {
const user = new PasswordUserWithOTP({
email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number({ style: "international" }),
isPhoneVerified: true,
password: "Password1!",
passwordChangeRequired: false,
type: OtpType.sms,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test.skip("DOESN'T WORK: username, password and sms otp login, enter code manually", async ({ user, page }) => {
// Given sms otp is enabled on the organization of the user
// Given the user has only sms otp configured as second factor
// User enters username
// User enters password
// User receives a sms with a verification code
// User enters the code into the ui
// User is redirected to the app (default redirect url)
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
await loginScreenExpect(page, user.getFullName());
});
test.skip("DOESN'T WORK: username, password and sms otp login, resend code", async ({ user, page }) => {
// Given sms otp is enabled on the organization of the user
// Given the user has only sms otp configured as second factor
// User enters username
// User enters password
// User receives a sms with a verification code
// User clicks resend code
// User receives a new sms with a verification code
// User is redirected to the app (default redirect url)
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
await loginScreenExpect(page, user.getFullName());
});
test("username, password and sms otp login, wrong code", async ({ user, page }) => {
// Given sms otp is enabled on the organization of the user
// Given the user has only sms otp configured as second factor
// User enters username
// User enters password
// User receives a sms with a verification code
// User enters a wrong code
// Error message - "Invalid code" is shown
const c = "wrongcode";
await loginWithPassword(page, user.getUsername(), user.getPassword());
await code(page, c);
await codeScreenExpect(page, c);
});

View File

@@ -0,0 +1,52 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { loginScreenExpect, loginWithPassword, startLogin } from "./login";
import { loginname } from "./loginname";
import { resetPassword, startResetPassword } from "./password";
import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.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: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("username and password set login", async ({ user, page }) => {
const changedPw = "ChangedPw1!";
await startLogin(page);
await loginname(page, user.getUsername());
await resetPassword(page, user.getUsername(), changedPw);
await loginScreenExpect(page, user.getFullName());
await loginWithPassword(page, user.getUsername(), changedPw);
await loginScreenExpect(page, user.getFullName());
});
test("password set not with desired complexity", async ({ user, page }) => {
const changedPw1 = "change";
const changedPw2 = "chang";
await startLogin(page);
await loginname(page, user.getUsername());
await startResetPassword(page);
await resetPasswordScreen(page, user.getUsername(), changedPw1, changedPw2);
await resetPasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
});

View File

@@ -0,0 +1,71 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { code } from "./code";
import { codeScreenExpect } from "./code-screen";
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "./login";
import { PasswordUserWithTOTP } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
user: async ({ page }, use) => {
const user = new PasswordUserWithTOTP({
email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number({ style: "international" }),
isPhoneVerified: true,
password: "Password1!",
passwordChangeRequired: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("username, password and totp login", async ({ user, page }) => {
// Given totp is enabled on the organization of the user
// Given the user has only totp configured as second factor
// User enters username
// User enters password
// Screen for entering the code is shown directly
// User enters the code into the ui
// User is redirected to the app (default redirect url)
await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret());
await loginScreenExpect(page, user.getFullName());
});
test("username, password and totp otp login, wrong code", async ({ user, page }) => {
// Given totp is enabled on the organization of the user
// Given the user has only totp configured as second factor
// User enters username
// User enters password
// Screen for entering the code is shown directly
// User enters a wrond code
// Error message - "Invalid code" is shown
const c = "wrongcode";
await loginWithPassword(page, user.getUsername(), user.getPassword());
await code(page, c);
await codeScreenExpect(page, c);
});
test("username, password and totp login, multiple mfa options", async ({ page }) => {
test.skip();
// Given totp and email otp is enabled on the organization of the user
// Given the user has totp and email otp configured as second factor
// User enters username
// User enters password
// Screen for entering the code is shown directly
// Button to switch to email otp is shown
// User clicks button to use email otp instead
// User receives an email with a verification code
// User enters code in ui
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,26 @@
import { test } from "@playwright/test";
test("username, password and u2f login", async ({ page }) => {
test.skip();
// Given u2f is enabled on the organization of the user
// Given the user has only u2f configured as second factor
// User enters username
// User enters password
// Popup for u2f is directly opened
// User verifies u2f
// User is redirected to the app (default redirect url)
});
test("username, password and u2f login, multiple mfa options", async ({ page }) => {
test.skip();
// Given u2f and semailms otp is enabled on the organization of the user
// Given the user has u2f and email otp configured as second factor
// User enters username
// User enters password
// Popup for u2f is directly opened
// User aborts u2f verification
// User clicks button to use email otp as second factor
// User receives an email with a verification code
// User enters code in ui
// User is redirected to the app (default redirect url)
});

View File

@@ -0,0 +1,157 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { loginScreenExpect, loginWithPassword, startLogin } from "./login";
import { loginname } from "./loginname";
import { loginnameScreenExpect } from "./loginname-screen";
import { password } from "./password";
import { passwordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.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: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("username and password login", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword());
await loginScreenExpect(page, user.getFullName());
});
test("username and password login, unknown username", async ({ page }) => {
const username = "unknown";
await startLogin(page);
await loginname(page, username);
await loginnameScreenExpect(page, username);
});
test("username and password login, wrong password", async ({ user, page }) => {
await startLogin(page);
await loginname(page, user.getUsername());
await password(page, "wrong");
await passwordScreenExpect(page, "wrong");
});
test("username and password login, wrong username, ignore unknown usernames", async ({ user, page }) => {
test.skip();
// Given user doesn't exist but ignore unknown usernames setting is set to true
// Given username password login is enabled on the users organization
// enter login name
// enter password
// redirect to loginname page --> error message username or password wrong
});
test("username and password login, initial password change", async ({ user, page }) => {
test.skip();
// Given user is created and has changePassword set to true
// Given username password login is enabled on the users organization
// enter login name
// enter password
// create new password
});
test("username and password login, reset password hidden", async ({ user, page }) => {
test.skip();
// Given the organization has enabled "Password reset hidden" in the login policy
// Given username password login is enabled on the users organization
// enter login name
// password reset link should not be shown on password screen
});
test("username and password login, reset password - enter code manually", async ({ user, page }) => {
test.skip();
// Given user has forgotten password and clicks the forgot password button
// Given username password login is enabled on the users organization
// enter login name
// click password forgotten
// enter code from email
// user is redirected to app (default redirect url)
});
test("username and password login, reset password - click link", async ({ user, page }) => {
test.skip();
// Given user has forgotten password and clicks the forgot password button, and then the link in the email
// Given username password login is enabled on the users organization
// enter login name
// click password forgotten
// click link in email
// set new password
// redirect to app (default redirect url)
});
test("username and password login, reset password, resend code", async ({ user, page }) => {
test.skip();
// Given user has forgotten password and clicks the forgot password button and then resend code
// Given username password login is enabled on the users organization
// enter login name
// click password forgotten
// click resend code
// enter code from second email
// user is redirected to app (default redirect url)
});
test("email login enabled", async ({ user, page }) => {
test.skip();
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
// Given no other user with the same email address exists
// enter email address "test@zitadel.com " in login screen
// user will get to password screen
});
test("email login disabled", async ({ user, page }) => {
test.skip();
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
// Given no other user with the same email address exists
// enter email address "test@zitadel.com" in login screen
// user will see error message "user not found"
});
test("email login enabled - multiple users", async ({ user, page }) => {
test.skip();
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
// Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists
// enter email address "test@zitadel.com" in login screen
// user will see error message "user not found"
});
test("phone login enabled", async ({ user, page }) => {
test.skip();
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
// Given no other user with the same phon number exists
// enter phone number "0711111111" in login screen
// user will get to password screen
});
test("phone login disabled", async ({ user, page }) => {
test.skip();
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
// Given no other user with the same phone number exists
// enter phone number "0711111111" in login screen
// user will see error message "user not found"
});
test("phone login enabled - multiple users", async ({ user, page }) => {
test.skip();
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
// Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists
// enter phone number "0711111111" in login screen
// user will see error message "user not found"
});

View File

@@ -0,0 +1,6 @@
import { test } from "@playwright/test";
test("login is accessible", async ({ page }) => {
await page.goto("./");
await page.getByRole("heading", { name: "Welcome back!" }).isVisible();
});

View File

@@ -0,0 +1,190 @@
import { Authenticator } from "@otplib/core";
import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin
import axios from "axios";
import dotenv from "dotenv";
import { request } from "gaxios";
import path from "path";
import { OtpType, userProps } from "./user";
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
export async function addUser(props: userProps) {
const body = {
username: props.email,
organization: {
orgId: props.organization,
},
profile: {
givenName: props.firstName,
familyName: props.lastName,
},
email: {
email: props.email,
isVerified: true,
},
phone: {
phone: props.phone,
isVerified: true,
},
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);
}
export async function removeUserByUsername(username: string) {
const resp = await getUserByUsername(username);
if (!resp || !resp.result || !resp.result[0]) {
return;
}
await removeUser(resp.result[0].userId);
}
export async function removeUser(id: string) {
await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`);
}
async function deleteCall(url: string) {
try {
const response = await axios.delete(url, {
headers: {
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
if (response.status >= 400 && response.status !== 404) {
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
console.error(error);
throw new Error(error);
}
} catch (error) {
console.error("Error making request:", error);
throw error;
}
}
export async function getUserByUsername(username: string): Promise<any> {
const listUsersBody = {
queries: [
{
userNameQuery: {
userName: username,
},
},
],
};
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody);
}
async function listCall(url: string, data: any): Promise<any> {
try {
const response = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
if (response.status >= 400) {
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
console.error(error);
throw new Error(error);
}
return response.data;
} catch (error) {
console.error("Error making request:", error);
throw error;
}
}
export async function activateOTP(userId: string, type: OtpType) {
let url = "otp_";
switch (type) {
case OtpType.sms:
url = url + "sms";
break;
case OtpType.email:
url = url + "email";
break;
}
await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {});
}
async function pushCall(url: string, data: any) {
try {
const response = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
if (response.status >= 400) {
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
console.error(error);
throw new Error(error);
}
} catch (error) {
console.error("Error making request:", error);
throw error;
}
}
export async function addTOTP(userId: string): Promise<string> {
const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {});
const code = totp(response.secret);
await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code });
return response.secret;
}
export function totp(secret: string) {
const authenticator = new Authenticator({
createDigest,
createRandomBytes,
keyDecoder,
keyEncoder,
});
// google authenticator usage
const token = authenticator.generate(secret);
// check if token can be used
if (!authenticator.verify({ token: token, secret: secret })) {
const error = `Generated token could not be verified`;
console.error(error);
throw new Error(error);
}
return token;
}
export async function eventualNewUser(id: string) {
return request({
url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`,
method: "GET",
headers: {
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
retryConfig: {
statusCodesToRetry: [[404, 404]],
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
totalTimeout: 10000, // 10 seconds
onRetryAttempt: (error) => {
console.warn(`Retrying to query new user ${id}: ${error.message}`);
},
},
});
}