diff --git a/.eslintrc.js b/.eslintrc.cjs
similarity index 100%
rename from .eslintrc.js
rename to .eslintrc.cjs
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 0ea84c5c8a..138d4919af 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -7,6 +7,7 @@
- [ ] All open todos and follow ups are defined in a new ticket and justified
- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented.
- [ ] Vitest unit tests ensure that components produce expected outputs on different inputs.
-- [ ] Cypress integration tests ensure that login app pages work as expected. The ZITADEL API is mocked.
+- [ ] Cypress integration tests ensure that login app pages work as expected on good and bad user inputs, ZITADEL responses or IDP redirects. The ZITADEL API is mocked, IDP redirects are simulated.
+- [ ] Playwright acceptances tests ensure that the happy paths of common user journeys work as expected. The ZITADEL API is not mocked but IDP redirects are simulated.
- [ ] No debug or dead code
- [ ] My code has no repetitions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 91c78a4db2..cac00bde39 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,8 +1,39 @@
name: Quality
-on: pull_request
+on:
+ pull_request:
+ schedule:
+ # Every morning at 6:00 AM CET
+ - cron: '0 4 * * *'
+ workflow_dispatch:
+ inputs:
+ target-env:
+ description: 'Zitadel target environment to run the acceptance tests against.'
+ required: true
+ type: choice
+ options:
+ - 'qa'
+ - 'prod'
jobs:
+ matrix:
+ # If the workflow is triggered by a schedule event, only the acceptance tests run against QA and Prod.
+ name: Matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.matrix.outputs.matrix }}
+ steps:
+ - name: Matrix
+ id: matrix
+ run: |
+ if [ -n "${{ github.event.schedule }}" ]; then
+ echo 'matrix=["test:acceptance:qa", "test:acceptance:prod"]' >> $GITHUB_OUTPUT
+ elif [ -n "${{ github.event.inputs.target-env }}" ]; then
+ echo 'matrix=["test:acceptance:${{ github.event.inputs.target-env }}"]' >> $GITHUB_OUTPUT
+ else
+ echo 'matrix=["format --check", "lint", "test:unit", "test:integration", "test:acceptance"]' >> $GITHUB_OUTPUT
+ fi
+
quality:
name: Ensure Quality
@@ -13,46 +44,29 @@ jobs:
permissions:
contents: "read"
+ needs:
+ - matrix
+
strategy:
fail-fast: false
matrix:
- command:
- - format --check
- - lint
- - test:unit
- - test:integration
+ command: ${{ fromJson( needs.matrix.outputs.matrix ) }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4.1.6
- - name: Setup Node.js 20.x
- uses: actions/setup-node@v4.0.2
- with:
- node-version: 20.x
+ - name: Setup Buf
+ uses: bufbuild/buf-setup-action@v1.45.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.0.0
- - uses: pnpm/action-setup@v4.0.0
- name: Install pnpm
- id: pnpm-install
+ - name: Setup Node.js 20.x
+ uses: actions/setup-node@v4.0.2
with:
- run_install: false
-
- - name: Get pnpm store directory
- id: pnpm-cache
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
-
- - uses: actions/cache@v4.0.2
- name: Setup pnpm cache
- with:
- path: ${{ env.STORE_PATH }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
+ node-version: 20.x
+ cache: 'pnpm'
- uses: actions/cache@v4.0.2
name: Setup Cypress binary cache
@@ -61,11 +75,52 @@ jobs:
key: ${{ runner.os }}-cypress-binary-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-cypress-binary-
- if: ${{ matrix.command }} == "test:integration"
+ # The Cypress binary cache needs to be updated together with the pnpm dependencies cache.
+ # That's why we don't conditionally cache it using if: ${{ matrix.command == 'test:integration' }}
- name: Install Dependencies
- run: pnpm install
+ run: pnpm install --frozen-lockfile
+
+ # We can cache the Playwright binary independently from the pnpm cache, because we install it separately.
+ # After pnpm install --frozen-lockfile, we can get the version so we only have to download the binary once per version.
+ - run: echo "PLAYWRIGHT_VERSION=$(npx playwright --version | cut -d ' ' -f 2)" >> $GITHUB_ENV
+ if: ${{ startsWith(matrix.command, 'test:acceptance') }}
+
+ - uses: actions/cache@v4.0.2
+ name: Setup Playwright binary cache
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-binary-${{ env.PLAYWRIGHT_VERSION }}
+ restore-keys: |
+ ${{ runner.os }}-playwright-binary-
+ if: ${{ startsWith(matrix.command, 'test:acceptance') }}
+
+ - name: Install Playwright Browsers
+ run: pnpm exec playwright install --with-deps
+ if: ${{ startsWith(matrix.command, 'test:acceptance') && steps.playwright-cache.outputs.cache-hit != 'true' }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ if: ${{ matrix.command == 'test:acceptance' }}
+
+ - name: Run ZITADEL
+ run: ZITADEL_DEV_UID=root pnpm run-sink
+ if: ${{ matrix.command == 'test:acceptance' }}
+
+ - name: Create Cloud Env File
+ run: |
+ if [ "${{ matrix.command }}" == "test:acceptance:prod" ]; then
+ echo "${{ secrets.ENV_FILE_CONTENT_ACCEPTANCE_PROD }}" | tee apps/login/.env.local acceptance/tests/.env.local > /dev/null
+ else
+ echo "${{ secrets.ENV_FILE_CONTENT_ACCEPTANCE_QA }}" | tee apps/login/.env.local acceptance/tests/.env.local > /dev/null
+ fi
+ if: ${{ matrix.command == 'test:acceptance:qa' || matrix.command == 'test:acceptance:prod' }}
+
+ - name: Create Production Build
+ run: pnpm build
+ if: ${{ startsWith(matrix.command, 'test:acceptance') }}
- name: Check
id: check
- run: pnpm ${{ matrix.command }}
+ run: pnpm ${{ contains(matrix.command, 'test:acceptance') && 'test:acceptance' || matrix.command }}
diff --git a/.gitignore b/.gitignore
index 978119f359..5cf3100aa4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,7 @@ packages/zitadel-server/src/app/proto
.idea
.vercel
.env*.local
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/.prettierignore b/.prettierignore
index c7dc50f307..a37ac4ff79 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,8 @@
.next/
+.changeset/
+.github/
dist/
+packages/zitadel-proto/google
+packages/zitadel-proto/protoc-gen-openapiv2
+packages/zitadel-proto/validate
packages/zitadel-proto/zitadel
diff --git a/.prettierrc.json b/.prettierrc
similarity index 99%
rename from .prettierrc.json
rename to .prettierrc
index 55a45eb397..6d0c388d7a 100644
--- a/.prettierrc.json
+++ b/.prettierrc
@@ -3,3 +3,4 @@
"trailingComma": "all",
"plugins": ["prettier-plugin-organize-imports"]
}
+
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 18038ed32b..1b4e0b5bd8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -84,10 +84,9 @@ The application is now available at `http://localhost:3000`
You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories:
- apps/login
+- packages/zitadel-proto
- packages/zitadel-client
-- packages/zitadel-server
-- packages/zitadel-react
-- packages/zitadel-next
+- packages/zitadel-node
- The projects root directory: all tests in the project are executed
In apps/login, these commands also spin up the application and a ZITADEL gRPC API mock server to run integration tests using [Cypress](https://www.cypress.io/) against them.
diff --git a/README.md b/README.md
index cfe1f022fb..b08fbba545 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,16 @@
-# ZITADEL TypeScript with Turborepo and Changesets
+# ZITADEL TypeScript with Turborepo
This repository contains all TypeScript and JavaScript packages and applications you need to create your own ZITADEL
Login UI.
-The repo makes use of the [build system Turbo](https://turbo.build/repo) and
-the [Changesets CLI for versioning the packages](https://github.com/changesets/changesets).
+
+
**⚠️ This repo and packages are in alpha state and subject to change ⚠️**
-The scope of functionality of this repo and packages is limited and under active development.
-Once the package structure is set and all APIs are fully implemented we'll move this repo to beta state.
+The scope of functionality of this repo and packages is under active development.
+
+The `@zitadel/client` package is using [@connectrpc/connect](https://github.com/connectrpc/connect-es#readme).
+
You can read the [contribution guide](/CONTRIBUTING.md) on how to contribute.
Questions can be raised in our [Discord channel](https://discord.gg/erh5Brh7jE) or as
a [GitHub issue](https://github.com/zitadel/typescript/issues).
@@ -28,11 +30,8 @@ We think the easiest path of getting up and running, is the following:
## Included Apps And Packages
- `login`: The login UI used by ZITADEL Cloud, powered by Next.js
-- `@zitadel/node`: core components for establishing node client connection, grpc stub
-- `@zitadel/client`: shared client utilities
+- `@zitadel/client`: shared client utilities for node and browser environments
- `@zitadel/proto`: shared protobuf types
-- `@zitadel/react`: shared React utilities and components built with tailwindcss
-- `@zitadel/next`: shared Next.js utilities
- `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo
- `eslint-config-zitadel`: ESLint preset
@@ -50,17 +49,19 @@ the features will be extended.
This list should show the current implementation state, and also what is missing.
You can already use the current state, and extend it with your needs.
+#### Features list
+
- [x] Local User Registration (with Password)
- [x] User Registration and Login with external Provider
- [x] Google
- - [ ] GitHub
- - [ ] GitHub Enterprise
+ - [x] GitHub
+ - [x] GitHub Enterprise
- [x] GitLab
- - [ ] GitLab Enterprise
- - [ ] Azure
- - [ ] Apple
- - [ ] Generic OIDC
- - [ ] Generic OAuth
+ - [x] GitLab Enterprise
+ - [x] Azure
+ - [x] Apple
+ - [x] Generic OIDC
+ - [x] Generic OAuth
- [ ] Generic JWT
- [ ] LDAP
- [ ] SAML SP
@@ -70,9 +71,10 @@ You can already use the current state, and extend it with your needs.
- [x] OTP: Email Code
- [x] OTP: SMS Code
- [ ] Password Change/Reset
-- [ ] Domain Discovery
+- [x] Domain Discovery
- [x] Branding
- OIDC Standard
+
- [x] Authorization Code Flow with PKCE
- [x] AuthRequest `hintUserId`
- [x] AuthRequest `loginHint`
@@ -84,12 +86,60 @@ You can already use the current state, and extend it with your needs.
- Scopes
- [x] `openid email profile address``
- [x] `offline access`
- - [ ] `urn:zitadel:iam:org:idp:id:{idp_id}`
+ - [x] `urn:zitadel:iam:org:idp:id:{idp_id}`
- [x] `urn:zitadel:iam:org:project:id:zitadel:aud`
- [x] `urn:zitadel:iam:org:id:{orgid}`
- [x] `urn:zitadel:iam:org:domain:primary:{domain}`
- [ ] AuthRequest UI locales
+ #### Flow diagram
+
+ This diagram shows the available pages and flows.
+
+ > Note that back navigation or retries are not displayed.
+
+```mermaid
+ flowchart TD
+ A[Start] --> register
+ A[Start] --> accounts
+ A[Start] --> loginname
+ loginname -- signInWithIDP --> idp-success
+ loginname -- signInWithIDP --> idp-failure
+ idp-success --> B[signedin]
+ loginname --> password
+ loginname -- hasPasskey --> passkey
+ loginname -- allowRegister --> register
+ passkey-add --passwordAllowed --> password
+ passkey -- hasPassword --> password
+ passkey --> B[signedin]
+ password -- hasMFA --> mfa
+ password -- allowPasskeys --> passkey-add
+ password -- reset --> password-set
+ email -- reset --> password-set
+ password-set --> B[signedin]
+ password-change --> B[signedin]
+ password -- userstate=initial --> password-change
+
+ mfa --> otp
+ otp --> B[signedin]
+ mfa--> u2f
+ u2f -->B[signedin]
+ register -- password/passkey --> B[signedin]
+ password --> B[signedin]
+ password-- forceMFA -->mfaset
+ mfaset --> u2fset
+ mfaset --> otpset
+ u2fset --> B[signedin]
+ otpset --> B[signedin]
+ accounts--> loginname
+ password -- not verified yet -->verify
+ register-- withpassword -->verify
+ passkey-- notVerified --> verify
+ verify --> B[signedin]
+```
+
+You can find a more detailed documentation of the different pages [here](./apps/login/readme.md).
+
## Tooling
- [TypeScript](https://www.typescriptlang.org/) for static type checking
@@ -119,22 +169,6 @@ settings. The [Changesets bot](https://github.com/apps/changeset-bot) should als
Read the [changesets documentation](https://github.com/changesets/changesets/blob/main/docs/automating-changesets.md)
for more information about this automation
-### NPM
-
-If you want to publish a package to the public npm registry and make them publicly available, this is already setup.
-
-To publish packages to a private npm organization scope, **remove** the following from each of the `package.json`'s
-
-```diff
-- "publishConfig": {
-- "access": "public"
-- },
-```
-
-### GitHub Package Registry
-
-See [working with the npm registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#publishing-a-package-using-publishconfig-in-the-packagejson-file)
-
### Run Login UI
To run the application make sure to install the dependencies with
@@ -143,32 +177,75 @@ To run the application make sure to install the dependencies with
pnpm install
```
-then setup the environment for the login application which needs a `.env.local` in `/apps/login`.
-Go to your instance and create a service user for the application having the `IAM_OWNER` manager role.
-This user is required to have access to create users on your primary organization and reading policy data so it can be
-restricted to your personal use case but we'll stick with `IAM_OWNER` for convenience. Create a PAT and copy the value to
-paste it under the `ZITADEL_SERVICE_USER_TOKEN` key.
-The file should look as follows:
-
-```
-ZITADEL_API_URL=[yourinstanceurl]
-ZITADEL_ORG_ID=[yourprimaryorg]
-ZITADEL_SERVICE_USER_TOKEN=[yourserviceuserpersonalaccesstoken]
-```
-
then generate the GRPC stubs with
```sh
pnpm generate
```
-and then run it with
+To run the application against a local ZITADEL instance, run the following command:
+
+```sh
+pnpm run-zitadel
+```
+
+This sets up ZITADEL using docker compose and writes the configuration to the file `apps/login/.env.local`.
+
+
+Alternatively, use another environment
+You can develop against any ZITADEL instance in which you have sufficient rights to execute the following steps.
+Just create or overwrite the file `apps/login/.env.local` yourself.
+Add your instances base URL to the file at the key `ZITADEL_API_URL`.
+Go to your instance and create a service user for the login application.
+The login application creates users on your primary organization and reads policy data.
+For the sake of simplicity, just make the service user an instance member with the role `IAM_OWNER`.
+Create a PAT and copy it to the file `apps/login/.env.local` using the key `ZITADEL_SERVICE_USER_TOKEN`.
+Also add the users ID to the file using the key `ZITADEL_SERVICE_USER_ID`.
+
+The file should look similar to this:
+
+```
+ZITADEL_API_URL=https://zitadel-tlx3du.us1.zitadel.cloud
+ZITADEL_SERVICE_USER_ID=289106423158521850
+ZITADEL_SERVICE_USER_TOKEN=1S6w48thfWFI2klgfwkCnhXJLf9FQ457E-_3H74ePQxfO3Af0Tm4V5Xi-ji7urIl_xbn-Rk
+```
+
+
+
+Start the login application in dev mode:
```sh
pnpm dev
```
Open the login application with your favorite browser at `localhost:3000`.
+Change the source code and see the changes live in your browser.
+
+Make sure the application still behaves as expected by running all tests
+
+```sh
+pnpm test
+```
+
+To satisfy your unique workflow requirements, check out the package.json in the root directory for more detailed scripts.
+
+### Run Login UI Acceptance tests
+
+To run the acceptance tests you need a running ZITADEL environment and a component which receives HTTP requests for the emails and sms's.
+This component should also be able to return the content of these notifications, as the codes and links are used in the login flows.
+There is a basic implementation in Golang available under [the sink package](./acceptance/sink).
+
+To setup ZITADEL with the additional Sink container for handling the notifications:
+
+```sh
+pnpm run-sink
+```
+
+Then you can start the acceptance tests with:
+
+```sh
+pnpm test:acceptance
+```
### Deploy to Vercel
diff --git a/acceptance/Dockerfile b/acceptance/Dockerfile
index a2283d09a9..36f6ba8f19 100644
--- a/acceptance/Dockerfile
+++ b/acceptance/Dockerfile
@@ -1,6 +1,5 @@
FROM golang:1.19-alpine
RUN apk add curl jq
-RUN go install github.com/zitadel/zitadel-tools@v0.4.0
COPY setup.sh /setup.sh
RUN chmod +x /setup.sh
ENTRYPOINT [ "/setup.sh" ]
diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml
index ac2fa512f5..de6990387d 100644
--- a/acceptance/docker-compose.yaml
+++ b/acceptance/docker-compose.yaml
@@ -1,40 +1,38 @@
-version: "3.8"
-
services:
zitadel:
user: "${ZITADEL_DEV_UID}"
- image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}"
+ image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.65.0}"
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
ports:
- "8080:8080"
volumes:
- - ./machinekey:/machinekey
+ - ./pat:/pat
- ./zitadel.yaml:/zitadel.yaml
depends_on:
db:
condition: "service_healthy"
db:
- image: "cockroachdb/cockroach:v22.2.2"
- command: "start-single-node --insecure --http-addr :9090"
+ restart: "always"
+ image: postgres:17.0-alpine3.19
+ environment:
+ - POSTGRES_USER=zitadel
+ - PGUSER=zitadel
+ - POSTGRES_DB=zitadel
+ - POSTGRES_HOST_AUTH_METHOD=trust
+ command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:9090/health?ready=1"]
+ test: [ "CMD-SHELL", "pg_isready" ]
interval: "10s"
timeout: "30s"
retries: 5
start_period: "20s"
ports:
- - "26257:26257"
- - "9090:9090"
+ - 5432:5432
wait_for_zitadel:
image: curlimages/curl:8.00.1
- command:
- [
- "/bin/sh",
- "-c",
- "i=0; while ! curl http://zitadel:8080/debug/ready && [ $$i -lt 30 ]; do sleep 1; i=$$((i+1)); done; [ $$i -eq 30 ] && exit 1 || exit 0",
- ]
+ command: /bin/sh -c "until curl -s -o /dev/null -i -f http://zitadel:8080/debug/ready; do echo 'waiting' && sleep 1; done; echo 'ready' && sleep 5;" || false
depends_on:
- zitadel
@@ -43,12 +41,29 @@ services:
container_name: setup
build: .
environment:
- KEY: /key/zitadel-admin-sa.json
- SERVICE: http://zitadel:8080
- WRITE_ENVIRONMENT_FILE: /apps/login/.env.acceptance
+ PAT_FILE: /pat/zitadel-admin-sa.pat
+ ZITADEL_API_INTERNAL_URL: http://zitadel:8080
+ WRITE_ENVIRONMENT_FILE: /apps/login/.env.local
+ WRITE_TEST_ENVIRONMENT_FILE: /acceptance/tests/.env.local
+ SINK_EMAIL_INTERNAL_URL: http://sink:3333/email
+ SINK_SMS_INTERNAL_URL: http://sink:3333/sms
+ SINK_NOTIFICATION_URL: http://localhost:3333/notification
volumes:
- - "./machinekey:/key"
+ - "./pat:/pat"
- "../apps/login:/apps/login"
+ - "../acceptance/tests:/acceptance/tests"
depends_on:
wait_for_zitadel:
condition: "service_completed_successfully"
+
+ sink:
+ image: golang:1.19-alpine
+ container_name: sink
+ command: go run /sink/main.go -port '3333' -email '/email' -sms '/sms' -notification '/notification'
+ ports:
+ - 3333:3333
+ volumes:
+ - "./sink:/sink"
+ depends_on:
+ setup:
+ condition: "service_completed_successfully"
diff --git a/acceptance/machinekey/.gitignore b/acceptance/machinekey/.gitignore
deleted file mode 100644
index 7c9f54d04c..0000000000
--- a/acceptance/machinekey/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-zitadel-admin-sa.json
diff --git a/acceptance/machinekey/.kitkeep b/acceptance/machinekey/.kitkeep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/acceptance/pat/.gitignore b/acceptance/pat/.gitignore
new file mode 100644
index 0000000000..f0fa09f556
--- /dev/null
+++ b/acceptance/pat/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitkeep
\ No newline at end of file
diff --git a/acceptance/machinekey/.gitkeep b/acceptance/pat/.gitkeep
similarity index 100%
rename from acceptance/machinekey/.gitkeep
rename to acceptance/pat/.gitkeep
diff --git a/acceptance/setup.sh b/acceptance/setup.sh
index d3db4f2554..e6277854a7 100755
--- a/acceptance/setup.sh
+++ b/acceptance/setup.sh
@@ -1,125 +1,112 @@
#!/bin/sh
-set -e
+set -ex
-KEY=${KEY:-./machinekey/zitadel-admin-sa.json}
-echo "Using key path ${KEY} to the instance admin service account."
+PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat}
+ZITADEL_API_PROTOCOL="${ZITADEL_API_PROTOCOL:-http}"
+ZITADEL_API_DOMAIN="${ZITADEL_API_DOMAIN:-localhost}"
+ZITADEL_API_PORT="${ZITADEL_API_PORT:-8080}"
+ZITADEL_API_URL="${ZITADEL_API_URL:-${ZITADEL_API_PROTOCOL}://${ZITADEL_API_DOMAIN}:${ZITADEL_API_PORT}}"
+ZITADEL_API_INTERNAL_URL="${ZITADEL_API_INTERNAL_URL:-${ZITADEL_API_URL}}"
+SINK_EMAIL_INTERNAL_URL="${SINK_EMAIL_INTERNAL_URL:-"http://sink:3333/email"}"
+SINK_SMS_INTERNAL_URL="${SINK_SMS_INTERNAL_URL:-"http://sink:3333/sms"}"
+SINK_NOTIFICATION_URL="${SINK_NOTIFICATION_URL:-"http://localhost:3333/notification"}"
-AUDIENCE=${AUDIENCE:-http://localhost:8080}
-echo "Using audience ${AUDIENCE} for which the key is used."
+if [ -z "${PAT}" ]; then
+ echo "Reading PAT from file ${PAT_FILE}"
+ PAT=$(cat ${PAT_FILE})
+fi
-SERVICE=${SERVICE:-$AUDIENCE}
-echo "Using the service ${SERVICE} to connect to ZITADEL. For example in docker compose this can differ from the audience."
+if [ -z "${ZITADEL_SERVICE_USER_ID}" ]; then
+ echo "Reading ZITADEL_SERVICE_USER_ID from userinfo endpoint"
+ USERINFO_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/oidc/v1/userinfo" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}")
+ echo "Received userinfo response: ${USERINFO_RESPONSE}"
+ ZITADEL_SERVICE_USER_ID=$(echo "${USERINFO_RESPONSE}" | jq --raw-output '.sub')
+fi
-WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.acceptance}
+#################################################################
+# Environment files
+#################################################################
+
+WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.local}
echo "Writing environment file to ${WRITE_ENVIRONMENT_FILE} when done."
+WRITE_TEST_ENVIRONMENT_FILE=${WRITE_TEST_ENVIRONMENT_FILE:-$(dirname "$0")/../acceptance/tests/.env.local}
+echo "Writing environment file to ${WRITE_TEST_ENVIRONMENT_FILE} when done."
-AUDIENCE_HOST="$(echo $AUDIENCE | cut -d/ -f3)"
-echo "Deferred the Host header ${AUDIENCE_HOST} which will be sent in requests that ZITADEL then maps to a virtual instance"
-
-JWT=$(zitadel-tools key2jwt --key ${KEY} --audience ${AUDIENCE})
-echo "Created JWT from Admin service account key ${JWT}"
-
-TOKEN_RESPONSE=$(curl -s --request POST \
- --url ${SERVICE}/oauth/v2/token \
- --header 'Content-Type: application/x-www-form-urlencoded' \
- --header "Host: ${AUDIENCE_HOST}" \
- --data grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
- --data scope='openid profile email urn:zitadel:iam:org:project:id:zitadel:aud' \
- --data assertion="${JWT}")
-echo "Got response from token endpoint:"
-echo "${TOKEN_RESPONSE}" | jq
-
-TOKEN=$(echo -n ${TOKEN_RESPONSE} | jq --raw-output '.access_token')
-echo "Extracted access token ${TOKEN}"
-
-ORG_RESPONSE=$(curl -s --request GET \
- --url ${SERVICE}/admin/v1/orgs/default \
- --header 'Accept: application/json' \
- --header "Authorization: Bearer ${TOKEN}" \
- --header "Host: ${AUDIENCE_HOST}")
-echo "Got default org response:"
-echo "${ORG_RESPONSE}" | jq
-
-ORG_ID=$(echo -n ${ORG_RESPONSE} | jq --raw-output '.org.id')
-echo "Extracted default org id ${ORG_ID}"
-
-echo "ZITADEL_API_URL=${AUDIENCE}
-ZITADEL_ORG_ID=${ORG_ID}
-ZITADEL_SERVICE_USER_TOKEN=${TOKEN}" > ${WRITE_ENVIRONMENT_FILE}
+echo "ZITADEL_API_URL=${ZITADEL_API_URL}
+ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID}
+ZITADEL_SERVICE_USER_TOKEN=${PAT}
+SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
+DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
cat ${WRITE_ENVIRONMENT_FILE}
-if ! grep -q 'localhost' ${WRITE_ENVIRONMENT_FILE}; then
- echo "Not developing against localhost, so creating a human user might not be necessary"
- exit 0
-fi
+echo "Wrote environment file ${WRITE_TEST_ENVIRONMENT_FILE}"
+cat ${WRITE_TEST_ENVIRONMENT_FILE}
-HUMAN_USER_USERNAME="zitadel-admin@zitadel.localhost"
-HUMAN_USER_PASSWORD="Password1!"
+#################################################################
+# SMS provider with HTTP
+#################################################################
-HUMAN_USER_PAYLOAD=$(cat << EOM
-{
- "userName": "${HUMAN_USER_USERNAME}",
- "profile": {
- "firstName": "ZITADEL",
- "lastName": "Admin",
- "displayName": "ZITADEL Admin",
- "preferredLanguage": "en"
- },
- "email": {
- "email": "zitadel-admin@zitadel.localhost",
- "isEmailVerified": true
- },
- "password": "${HUMAN_USER_PASSWORD}",
- "passwordChangeRequired": false
-}
-EOM
-)
-echo "Creating human user"
-echo "${HUMAN_USER_PAYLOAD}" | jq
+SMSHTTP_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/http" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json" \
+ -d "{\"endpoint\": \"${SINK_SMS_INTERNAL_URL}\", \"description\": \"test\"}")
+echo "Received SMS HTTP response: ${SMSHTTP_RESPONSE}"
-HUMAN_USER_RESPONSE=$(curl -s --request POST \
- --url ${SERVICE}/management/v1/users/human/_import \
- --header 'Content-Type: application/json' \
- --header 'Accept: application/json' \
- --header "Authorization: Bearer ${TOKEN}" \
- --header "Host: ${AUDIENCE_HOST}" \
- --data-raw "${HUMAN_USER_PAYLOAD}")
-echo "Create human user response"
-echo "${HUMAN_USER_RESPONSE}" | jq
+SMSHTTP_ID=$(echo ${SMSHTTP_RESPONSE} | jq -r '. | .id')
+echo "Received SMS HTTP ID: ${SMSHTTP_ID}"
-if [ "$(echo -n "${HUMAN_USER_RESPONSE}" | jq --raw-output '.code')" == "6" ]; then
- echo "admin user already exists"
- exit 0
-fi
+SMS_ACTIVE_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/${SMSHTTP_ID}/_activate" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json")
+echo "Received SMS active response: ${SMS_ACTIVE_RESPONSE}"
-HUMAN_USER_ID=$(echo -n ${HUMAN_USER_RESPONSE} | jq --raw-output '.userId')
-echo "Extracted human user id ${HUMAN_USER_ID}"
+#################################################################
+# Email provider with HTTP
+#################################################################
-HUMAN_ADMIN_PAYLOAD=$(cat << EOM
-{
- "userId": "${HUMAN_USER_ID}",
- "roles": [
- "IAM_OWNER"
- ]
-}
-EOM
-)
-echo "Granting iam owner to human user"
-echo "${HUMAN_ADMIN_PAYLOAD}" | jq
+EMAILHTTP_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/http" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json" \
+ -d "{\"endpoint\": \"${SINK_EMAIL_INTERNAL_URL}\", \"description\": \"test\"}")
+echo "Received Email HTTP response: ${EMAILHTTP_RESPONSE}"
-HUMAN_ADMIN_RESPONSE=$(curl -s --request POST \
- --url ${SERVICE}/admin/v1/members \
- --header 'Content-Type: application/json' \
- --header 'Accept: application/json' \
- --header "Authorization: Bearer ${TOKEN}" \
- --header "Host: ${AUDIENCE_HOST}" \
- --data-raw "${HUMAN_ADMIN_PAYLOAD}")
+EMAILHTTP_ID=$(echo ${EMAILHTTP_RESPONSE} | jq -r '. | .id')
+echo "Received Email HTTP ID: ${EMAILHTTP_ID}"
-echo "Grant iam owner to human user response"
-echo "${HUMAN_ADMIN_RESPONSE}" | jq
+EMAIL_ACTIVE_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/${EMAILHTTP_ID}/_activate" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json")
+echo "Received Email active response: ${EMAIL_ACTIVE_RESPONSE}"
+
+#################################################################
+# Wait for projection of default organization in ZITADEL
+#################################################################
+
+DEFAULTORG_RESPONSE_RESULTS=0
+# waiting for default organization
+until [ ${DEFAULTORG_RESPONSE_RESULTS} -eq 1 ]
+do
+ DEFAULTORG_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/v2/organizations/_search" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json" \
+ -d "{\"queries\": [{\"defaultQuery\":{}}]}" )
+ echo "Received default organization response: ${DEFAULTORG_RESPONSE}"
+ DEFAULTORG_RESPONSE_RESULTS=$(echo $DEFAULTORG_RESPONSE | jq -r '.result | length')
+ echo "Received default organization response result: ${DEFAULTORG_RESPONSE_RESULTS}"
+done
-echo "You can now log in at ${AUDIENCE}/ui/login"
-echo "username: ${HUMAN_USER_USERNAME}"
-echo "password: ${HUMAN_USER_PASSWORD}"
\ No newline at end of file
diff --git a/acceptance/sink/go.mod b/acceptance/sink/go.mod
new file mode 100644
index 0000000000..a33d6ae8bd
--- /dev/null
+++ b/acceptance/sink/go.mod
@@ -0,0 +1,3 @@
+module github.com/zitadel/typescript/acceptance/sink
+
+go 1.22.6
diff --git a/acceptance/sink/main.go b/acceptance/sink/main.go
new file mode 100644
index 0000000000..d591981a34
--- /dev/null
+++ b/acceptance/sink/main.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+type serializableData struct {
+ ContextInfo map[string]interface{} `json:"contextInfo,omitempty"`
+ Args map[string]interface{} `json:"args,omitempty"`
+}
+
+type response struct {
+ Recipient string `json:"recipient,omitempty"`
+}
+
+func main() {
+ port := flag.String("port", "3333", "used port for the sink")
+ email := flag.String("email", "/email", "path for a sent email")
+ emailKey := flag.String("email-key", "recipientEmailAddress", "value in the sent context info of the email used as key to retrieve the notification")
+ sms := flag.String("sms", "/sms", "path for a sent sms")
+ smsKey := flag.String("sms-key", "recipientPhoneNumber", "value in the sent context info of the sms used as key to retrieve the notification")
+ notification := flag.String("notification", "/notification", "path to receive the notification")
+ flag.Parse()
+
+ messages := make(map[string]serializableData)
+
+ http.HandleFunc(*email, func(w http.ResponseWriter, r *http.Request) {
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ serializableData := serializableData{}
+ if err := json.Unmarshal(data, &serializableData); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ email, ok := serializableData.ContextInfo[*emailKey].(string)
+ if !ok {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ fmt.Println(email + ": " + string(data))
+ messages[email] = serializableData
+ io.WriteString(w, "Email!\n")
+ })
+
+ http.HandleFunc(*sms, func(w http.ResponseWriter, r *http.Request) {
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ serializableData := serializableData{}
+ if err := json.Unmarshal(data, &serializableData); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ phone, ok := serializableData.ContextInfo[*smsKey].(string)
+ if !ok {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ fmt.Println(phone + ": " + string(data))
+ messages[phone] = serializableData
+ io.WriteString(w, "SMS!\n")
+ })
+
+ http.HandleFunc(*notification, func(w http.ResponseWriter, r *http.Request) {
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ response := response{}
+ if err := json.Unmarshal(data, &response); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ serializableData, err := json.Marshal(messages[response.Recipient])
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ io.WriteString(w, string(serializableData))
+ })
+
+ fmt.Println("Starting server on", *port)
+ fmt.Println(*email, " for email handling")
+ fmt.Println(*sms, " for sms handling")
+ fmt.Println(*notification, " for retrieving notifications")
+ err := http.ListenAndServe(":"+*port, nil)
+ if err != nil {
+ panic("Server could not be started: " + err.Error())
+ }
+}
diff --git a/acceptance/tests/admin.spec.ts b/acceptance/tests/admin.spec.ts
new file mode 100644
index 0000000000..7ca28e4419
--- /dev/null
+++ b/acceptance/tests/admin.spec.ts
@@ -0,0 +1,7 @@
+import { test } from "@playwright/test";
+import { loginScreenExpect, loginWithPassword } from "./login";
+
+test("admin login", async ({ page }) => {
+ await loginWithPassword(page, "zitadel-admin@zitadel.localhost", "Password1!");
+ await loginScreenExpect(page, "ZITADEL Admin");
+});
diff --git a/acceptance/tests/code-screen.ts b/acceptance/tests/code-screen.ts
new file mode 100644
index 0000000000..3ab9dad26d
--- /dev/null
+++ b/acceptance/tests/code-screen.ts
@@ -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");
+}
diff --git a/acceptance/tests/code.ts b/acceptance/tests/code.ts
new file mode 100644
index 0000000000..1ae8f69791
--- /dev/null
+++ b/acceptance/tests/code.ts
@@ -0,0 +1,19 @@
+import { Page } from "@playwright/test";
+import { codeScreen } from "./code-screen";
+import { getOtpFromSink } from "./sink";
+
+export async function otpFromSink(page: Page, key: string) {
+ // wait for send of the code
+ await page.waitForTimeout(3000);
+ 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();
+}
diff --git a/acceptance/tests/idp-apple.spec.ts b/acceptance/tests/idp-apple.spec.ts
new file mode 100644
index 0000000000..89ed734ece
--- /dev/null
+++ b/acceptance/tests/idp-apple.spec.ts
@@ -0,0 +1,94 @@
+// 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-generic-jwt.spec.ts b/acceptance/tests/idp-generic-jwt.spec.ts
new file mode 100644
index 0000000000..054c147844
--- /dev/null
+++ b/acceptance/tests/idp-generic-jwt.spec.ts
@@ -0,0 +1,91 @@
+import test from "@playwright/test";
+
+test("login with Generic JWT IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-generic-oauth.spec.ts b/acceptance/tests/idp-generic-oauth.spec.ts
new file mode 100644
index 0000000000..7973e67c14
--- /dev/null
+++ b/acceptance/tests/idp-generic-oauth.spec.ts
@@ -0,0 +1,91 @@
+import test from "@playwright/test";
+
+test("login with Generic OAuth IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-generic-oidc.spec.ts b/acceptance/tests/idp-generic-oidc.spec.ts
new file mode 100644
index 0000000000..4ed536f613
--- /dev/null
+++ b/acceptance/tests/idp-generic-oidc.spec.ts
@@ -0,0 +1,93 @@
+// 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-github-enterprise.spec.ts b/acceptance/tests/idp-github-enterprise.spec.ts
new file mode 100644
index 0000000000..0a567c444b
--- /dev/null
+++ b/acceptance/tests/idp-github-enterprise.spec.ts
@@ -0,0 +1,95 @@
+import test from "@playwright/test";
+
+test("login with GitHub Enterprise IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-github.spec.ts b/acceptance/tests/idp-github.spec.ts
new file mode 100644
index 0000000000..be36a542f0
--- /dev/null
+++ b/acceptance/tests/idp-github.spec.ts
@@ -0,0 +1,95 @@
+import test from "@playwright/test";
+
+test("login with GitHub IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-gitlab-self-hosted.spec.ts b/acceptance/tests/idp-gitlab-self-hosted.spec.ts
new file mode 100644
index 0000000000..d254062e2a
--- /dev/null
+++ b/acceptance/tests/idp-gitlab-self-hosted.spec.ts
@@ -0,0 +1,95 @@
+import test from "@playwright/test";
+
+test("login with GitLab Self-Hosted IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-gitlab.spec.ts b/acceptance/tests/idp-gitlab.spec.ts
new file mode 100644
index 0000000000..e38b70a1e1
--- /dev/null
+++ b/acceptance/tests/idp-gitlab.spec.ts
@@ -0,0 +1,95 @@
+import test from "@playwright/test";
+
+test("login with GitLab IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-google.spec.ts b/acceptance/tests/idp-google.spec.ts
new file mode 100644
index 0000000000..c6219722a9
--- /dev/null
+++ b/acceptance/tests/idp-google.spec.ts
@@ -0,0 +1,91 @@
+import test from "@playwright/test";
+
+test("login with Google IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-ldap.spec.ts b/acceptance/tests/idp-ldap.spec.ts
new file mode 100644
index 0000000000..9ab0310fa0
--- /dev/null
+++ b/acceptance/tests/idp-ldap.spec.ts
@@ -0,0 +1,91 @@
+import test from "@playwright/test";
+
+test("login with LDAP IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-microsoft.spec.ts b/acceptance/tests/idp-microsoft.spec.ts
new file mode 100644
index 0000000000..40d44d577f
--- /dev/null
+++ b/acceptance/tests/idp-microsoft.spec.ts
@@ -0,0 +1,94 @@
+// 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/idp-saml.spec.ts b/acceptance/tests/idp-saml.spec.ts
new file mode 100644
index 0000000000..e9e145909c
--- /dev/null
+++ b/acceptance/tests/idp-saml.spec.ts
@@ -0,0 +1,95 @@
+import test from "@playwright/test";
+
+test("login with SAML IDP", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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,
+}) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/login-configuration-possiblities.spec.ts b/acceptance/tests/login-configuration-possiblities.spec.ts
new file mode 100644
index 0000000000..f82f99364a
--- /dev/null
+++ b/acceptance/tests/login-configuration-possiblities.spec.ts
@@ -0,0 +1,51 @@
+import test from "@playwright/test";
+
+test("login with mfa setup, mfa setup prompt", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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"
+});
diff --git a/acceptance/tests/login.ts b/acceptance/tests/login.ts
new file mode 100644
index 0000000000..32c0007a3c
--- /dev/null
+++ b/acceptance/tests/login.ts
@@ -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));
+}
diff --git a/acceptance/tests/loginname-screen.ts b/acceptance/tests/loginname-screen.ts
new file mode 100644
index 0000000000..be41a28eda
--- /dev/null
+++ b/acceptance/tests/loginname-screen.ts
@@ -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");
+}
diff --git a/acceptance/tests/loginname.ts b/acceptance/tests/loginname.ts
new file mode 100644
index 0000000000..2050ec1d3c
--- /dev/null
+++ b/acceptance/tests/loginname.ts
@@ -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();
+}
diff --git a/acceptance/tests/passkey.ts b/acceptance/tests/passkey.ts
new file mode 100644
index 0000000000..d8cda10ddb
--- /dev/null
+++ b/acceptance/tests/passkey.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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,
+) {
+ // initialize event listeners to wait for a successful passkey input event
+ const operationCompleted = new Promise((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,
+) {
+ // initialize event listeners to wait for a successful passkey input event
+ const operationCompleted = new Promise((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;
+}
diff --git a/acceptance/tests/password-screen.ts b/acceptance/tests/password-screen.ts
new file mode 100644
index 0000000000..57334a07d2
--- /dev/null
+++ b/acceptance/tests/password-screen.ts
@@ -0,0 +1,82 @@
+import { expect, Page } from "@playwright/test";
+import { getCodeFromSink } from "./sink";
+
+const codeField = "code-text-input";
+const passwordField = "password-text-input";
+const passwordConfirmField = "password-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(passwordField).pressSequentially(password1);
+ await page.getByTestId(passwordConfirmField).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("Could not verify password");
+}
+
+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(passwordField)).toHaveValue(password1);
+ await expect(page.getByTestId(passwordConfirmField)).toHaveValue(password2);
+
+ 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) {
+ // wait for send of the code
+ await page.waitForTimeout(3000);
+ const c = await getCodeFromSink(username);
+ await page.getByTestId(codeField).pressSequentially(c);
+ await page.getByTestId(passwordField).pressSequentially(password1);
+ await page.getByTestId(passwordConfirmField).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 changePasswordScreenExpect(page, password1, password2, length, symbol, number, uppercase, lowercase, equals);
+}
diff --git a/acceptance/tests/password.ts b/acceptance/tests/password.ts
new file mode 100644
index 0000000000..b3d31fcaee
--- /dev/null
+++ b/acceptance/tests/password.ts
@@ -0,0 +1,30 @@
+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, loginname: string, password: string) {
+ await startChangePassword(page, loginname);
+ 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();
+}
diff --git a/acceptance/tests/register-screen.ts b/acceptance/tests/register-screen.ts
new file mode 100644
index 0000000000..d14f5dc970
--- /dev/null
+++ b/acceptance/tests/register-screen.ts
@@ -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();
+}
diff --git a/acceptance/tests/register.spec.ts b/acceptance/tests/register.spec.ts
new file mode 100644
index 0000000000..a3ffc7a67e
--- /dev/null
+++ b/acceptance/tests/register.spec.ts
@@ -0,0 +1,173 @@
+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, ".env.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(2000);
+ 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(2000);
+ await removeUserByUsername(username);
+});
+
+test("register with username and password - only password enabled", async ({ page }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/register.ts b/acceptance/tests/register.ts
new file mode 100644
index 0000000000..693bdfbc0d
--- /dev/null
+++ b/acceptance/tests/register.ts
@@ -0,0 +1,29 @@
+import { Page } from "@playwright/test";
+import { passkeyRegister } from "./passkey";
+import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
+
+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();
+}
+
+export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise {
+ await page.goto("/register");
+ await registerUserScreenPasskey(page, firstname, lastname, email);
+ await page.getByTestId("submit-button").click();
+
+ // wait for projection of user
+ await page.waitForTimeout(2000);
+
+ return await passkeyRegister(page);
+}
diff --git a/acceptance/tests/sink.ts b/acceptance/tests/sink.ts
new file mode 100644
index 0000000000..fc13a98dc7
--- /dev/null
+++ b/acceptance/tests/sink.ts
@@ -0,0 +1,55 @@
+import axios from "axios";
+
+export async function getOtpFromSink(key: string): Promise {
+ try {
+ const response = await axios.post(
+ process.env.SINK_NOTIFICATION_URL!,
+ {
+ recipient: key,
+ },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
+ },
+ },
+ );
+
+ if (response.status >= 400) {
+ const error = `HTTP Error: ${response.status} - ${response.statusText}`;
+ console.error(error);
+ throw new Error(error);
+ }
+ return response.data.args.oTP;
+ } catch (error) {
+ console.error("Error making request:", error);
+ throw error;
+ }
+}
+
+export async function getCodeFromSink(key: string): Promise {
+ try {
+ const response = await axios.post(
+ process.env.SINK_NOTIFICATION_URL!,
+ {
+ recipient: key,
+ },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
+ },
+ },
+ );
+
+ if (response.status >= 400) {
+ const error = `HTTP Error: ${response.status} - ${response.statusText}`;
+ console.error(error);
+ throw new Error(error);
+ }
+ return response.data.args.code;
+ } catch (error) {
+ console.error("Error making request:", error);
+ throw error;
+ }
+}
diff --git a/acceptance/tests/user.ts b/acceptance/tests/user.ts
new file mode 100644
index 0000000000..3daefdff08
--- /dev/null
+++ b/acceptance/tests/user.ts
@@ -0,0 +1,171 @@
+import { Page } from "@playwright/test";
+import { registerWithPasskey } from "./register";
+import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./zitadel";
+
+export interface userProps {
+ email: string;
+ firstName: string;
+ lastName: string;
+ organization: string;
+ password: string;
+ phone: string;
+}
+
+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);
+ // wait for projection of user
+ await page.waitForTimeout(2000);
+ }
+}
+
+export enum OtpType {
+ sms = "sms",
+ email = "email",
+}
+
+export interface otpUserProps {
+ email: string;
+ firstName: string;
+ lastName: string;
+ organization: string;
+ password: string;
+ phone: string;
+ 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,
+ });
+ this.type = props.type;
+ }
+
+ async ensure(page: Page) {
+ await super.ensure(page);
+
+ await activateOTP(this.getUserId(), this.type);
+
+ // wait for projection of user
+ await page.waitForTimeout(2000);
+ }
+}
+
+export class PasswordUserWithTOTP extends User {
+ private secret: string;
+
+ async ensure(page: Page) {
+ await super.ensure(page);
+
+ this.secret = await addTOTP(this.getUserId());
+
+ // wait for projection of user
+ await page.waitForTimeout(2000);
+ }
+
+ public getSecret(): string {
+ return this.secret;
+ }
+}
+
+export interface passkeyUserProps {
+ email: string;
+ firstName: string;
+ lastName: string;
+ organization: string;
+ phone: string;
+}
+
+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,
+ });
+ }
+
+ 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(2000);
+ }
+
+ 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;
+ }
+}
diff --git a/acceptance/tests/username-passkey.spec.ts b/acceptance/tests/username-passkey.spec.ts
new file mode 100644
index 0000000000..e73de3547f
--- /dev/null
+++ b/acceptance/tests/username-passkey.spec.ts
@@ -0,0 +1,40 @@
+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, ".env.local") });
+
+const test = base.extend<{ user: PasskeyUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasskeyUser({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ });
+ 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 }) => {
+ // 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
+});
diff --git a/acceptance/tests/username-password-changed.spec.ts b/acceptance/tests/username-password-changed.spec.ts
new file mode 100644
index 0000000000..c185e51ec9
--- /dev/null
+++ b/acceptance/tests/username-password-changed.spec.ts
@@ -0,0 +1,53 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginWithPassword } from "./login";
+import { startChangePassword } from "./password";
+import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
+import { PasswordUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, ".env.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ password: "Password1!",
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and password changed login", async ({ user, page }) => {
+ // commented, fix in https://github.com/zitadel/zitadel/pull/8807
+ /*
+ const changedPw = "ChangedPw1!";
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+
+ // wait for projection of token
+ await page.waitForTimeout(2000);
+
+ await changePassword(page, user.getUsername(), 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);
+});
diff --git a/acceptance/tests/username-password-otp_email.spec.ts b/acceptance/tests/username-password-otp_email.spec.ts
new file mode 100644
index 0000000000..daa2e0a429
--- /dev/null
+++ b/acceptance/tests/username-password-otp_email.spec.ts
@@ -0,0 +1,93 @@
+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, ".env.local") });
+
+const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUserWithOTP({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ password: "Password1!",
+ type: OtpType.email,
+ });
+
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("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 }) => {
+ // 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("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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/username-password-otp_sms.spec.ts b/acceptance/tests/username-password-otp_sms.spec.ts
new file mode 100644
index 0000000000..8fb91a66c7
--- /dev/null
+++ b/acceptance/tests/username-password-otp_sms.spec.ts
@@ -0,0 +1,68 @@
+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, ".env.local") });
+
+const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUserWithOTP({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number({ style: "international" }),
+ password: "Password1!",
+ type: OtpType.sms,
+ });
+
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("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("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);
+});
diff --git a/acceptance/tests/username-password-set.spec.ts b/acceptance/tests/username-password-set.spec.ts
new file mode 100644
index 0000000000..30d442df50
--- /dev/null
+++ b/acceptance/tests/username-password-set.spec.ts
@@ -0,0 +1,51 @@
+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, ".env.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ password: "Password1!",
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and password set login", async ({ user, page }) => {
+ // commented, fix in https://github.com/zitadel/zitadel/pull/8807
+
+ 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);
+});
diff --git a/acceptance/tests/username-password-totp.spec.ts b/acceptance/tests/username-password-totp.spec.ts
new file mode 100644
index 0000000000..4b6e678931
--- /dev/null
+++ b/acceptance/tests/username-password-totp.spec.ts
@@ -0,0 +1,67 @@
+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, ".env.local") });
+
+const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUserWithTOTP({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number({ style: "international" }),
+ password: "Password1!",
+ });
+
+ 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/username-password-u2f.spec.ts b/acceptance/tests/username-password-u2f.spec.ts
new file mode 100644
index 0000000000..f6f918478f
--- /dev/null
+++ b/acceptance/tests/username-password-u2f.spec.ts
@@ -0,0 +1,24 @@
+import { test } from "@playwright/test";
+
+test("username, password and u2f login", async ({ page }) => {
+ // 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 }) => {
+ // 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)
+});
diff --git a/acceptance/tests/username-password.spec.ts b/acceptance/tests/username-password.spec.ts
new file mode 100644
index 0000000000..fcb6aad037
--- /dev/null
+++ b/acceptance/tests/username-password.spec.ts
@@ -0,0 +1,142 @@
+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, ".env.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ password: "Password1!",
+ });
+ 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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 }) => {
+ // 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"
+});
diff --git a/acceptance/tests/welcome.ts b/acceptance/tests/welcome.ts
new file mode 100644
index 0000000000..7ff6b7d1c5
--- /dev/null
+++ b/acceptance/tests/welcome.ts
@@ -0,0 +1,6 @@
+import { test } from "@playwright/test";
+
+test("login is accessible", async ({ page }) => {
+ await page.goto("http://localhost:3000/");
+ await page.getByRole("heading", { name: "Welcome back!" }).isVisible();
+});
diff --git a/acceptance/tests/zitadel.ts b/acceptance/tests/zitadel.ts
new file mode 100644
index 0000000000..587723f10d
--- /dev/null
+++ b/acceptance/tests/zitadel.ts
@@ -0,0 +1,159 @@
+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 { OtpType, userProps } from "./user";
+
+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!,
+ },
+ };
+
+ 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_SERVICE_USER_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 {
+ 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 {
+ try {
+ const response = await axios.post(url, data, {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_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_SERVICE_USER_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 {
+ 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;
+}
diff --git a/acceptance/zitadel.yaml b/acceptance/zitadel.yaml
index 2b471db040..5a17264eb6 100644
--- a/acceptance/zitadel.yaml
+++ b/acceptance/zitadel.yaml
@@ -1,22 +1,49 @@
FirstInstance:
- MachineKeyPath: /machinekey/zitadel-admin-sa.json
+ PatPath: /pat/zitadel-admin-sa.pat
Org:
+ Human:
+ UserName: zitadel-admin
+ FirstName: ZITADEL
+ LastName: Admin
+ Password: Password1!
+ PasswordChangeRequired: false
+ PreferredLanguage: en
Machine:
Machine:
Username: zitadel-admin-sa
Name: Admin
- MachineKey:
- Type: 1
+ Pat:
+ ExpirationDate: 2099-01-01T00:00:00Z
+
+DefaultInstance:
+ PrivacyPolicy:
+ TOSLink: "https://zitadel.com/docs/legal/terms-of-service"
+ PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy"
+ HelpLink: "https://zitadel.com/docs"
+ SupportEmail: "support@zitadel.com"
+ DocsLink: "https://zitadel.com/docs"
Database:
- Cockroach:
+ EventPushConnRatio: 0.2 # 4
+ ProjectionSpoolerConnRatio: 0.3 # 6
+ postgres:
Host: db
+ Port: 5432
+ Database: zitadel
+ MaxOpenConns: 20
+ MaxIdleConns: 20
+ MaxConnLifetime: 1h
+ MaxConnIdleTime: 5m
+ User:
+ Username: zitadel
+ SSL:
+ Mode: disable
+ Admin:
+ Username: zitadel
+ SSL:
+ Mode: disable
Logstore:
Access:
Stdout:
Enabled: true
-
-DefaultInstance:
- LoginPolicy:
- MfaInitSkipLifetime: 0h
\ No newline at end of file
diff --git a/apps/login/.env.integration b/apps/login/.env.integration
index 44e8f7b6bf..90adb84eee 100644
--- a/apps/login/.env.integration
+++ b/apps/login/.env.integration
@@ -1 +1,3 @@
-ZITADEL_API_URL=http://localhost:22222
\ No newline at end of file
+ZITADEL_API_URL=http://localhost:22222
+EMAIL_VERIFICATION=true
+DEBUG=true
\ No newline at end of file
diff --git a/apps/login/.eslintrc.js b/apps/login/.eslintrc.cjs
similarity index 71%
rename from apps/login/.eslintrc.js
rename to apps/login/.eslintrc.cjs
index 8cf53b3cd8..f5383dd47a 100755
--- a/apps/login/.eslintrc.js
+++ b/apps/login/.eslintrc.cjs
@@ -4,4 +4,9 @@ module.exports = {
rules: {
"@next/next/no-html-link-for-pages": "off",
},
+ settings: {
+ react: {
+ version: "detect",
+ },
+ },
};
diff --git a/apps/login/.gitignore b/apps/login/.gitignore
new file mode 100644
index 0000000000..63ddd0c9eb
--- /dev/null
+++ b/apps/login/.gitignore
@@ -0,0 +1,2 @@
+custom-config.js
+.env.local
\ No newline at end of file
diff --git a/apps/login/cypress/integration/invite.cy.ts b/apps/login/cypress/integration/invite.cy.ts
new file mode 100644
index 0000000000..3014f5a2e5
--- /dev/null
+++ b/apps/login/cypress/integration/invite.cy.ts
@@ -0,0 +1,114 @@
+import { stub } from "../support/mock";
+
+describe("verify invite", () => {
+ beforeEach(() => {
+ stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [{ id: "256088834543534543" }],
+ },
+ });
+
+ stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
+ data: {
+ authMethodTypes: [], // user with no auth methods was invited
+ },
+ });
+
+ stub("zitadel.user.v2.UserService", "GetUserByID", {
+ data: {
+ user: {
+ userId: "221394658884845598",
+ state: 1,
+ username: "john@zitadel.com",
+ loginNames: ["john@zitadel.com"],
+ preferredLoginName: "john@zitadel.com",
+ human: {
+ userId: "221394658884845598",
+ state: 1,
+ username: "john@zitadel.com",
+ loginNames: ["john@zitadel.com"],
+ preferredLoginName: "john@zitadel.com",
+ profile: {
+ givenName: "John",
+ familyName: "Doe",
+ avatarUrl: "https://zitadel.com/avatar.jpg",
+ },
+ email: {
+ email: "john@zitadel.com",
+ isVerified: false,
+ },
+ },
+ },
+ },
+ });
+
+ stub("zitadel.session.v2.SessionService", "CreateSession", {
+ data: {
+ details: {
+ sequence: 859,
+ changeDate: new Date("2024-04-04T09:40:55.577Z"),
+ resourceOwner: "220516472055706145",
+ },
+ sessionId: "221394658884845598",
+ sessionToken:
+ "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
+ challenges: undefined,
+ },
+ });
+
+ stub("zitadel.session.v2.SessionService", "GetSession", {
+ data: {
+ session: {
+ id: "221394658884845598",
+ creationDate: new Date("2024-04-04T09:40:55.577Z"),
+ changeDate: new Date("2024-04-04T09:40:55.577Z"),
+ sequence: 859,
+ factors: {
+ user: {
+ id: "221394658884845598",
+ loginName: "john@zitadel.com",
+ },
+ password: undefined,
+ webAuthN: undefined,
+ intent: undefined,
+ },
+ metadata: {},
+ },
+ },
+ });
+
+ stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
+ data: {
+ settings: {
+ passkeysType: 1,
+ allowUsernamePassword: true,
+ },
+ },
+ });
+ });
+
+ it.only("shows authenticators after successful invite verification", () => {
+ stub("zitadel.user.v2.UserService", "VerifyInviteCode");
+
+ cy.visit("/verify?userId=221394658884845598&code=abc&invite=true");
+ cy.location("pathname", { timeout: 10_000 }).should(
+ "eq",
+ "/authenticator/set",
+ );
+ });
+
+ it("shows an error if invite code validation failed", () => {
+ stub("zitadel.user.v2.UserService", "VerifyInviteCode", {
+ code: 3,
+ error: "error validating code",
+ });
+
+ // TODO: Avoid uncaught exception in application
+ cy.once("uncaught:exception", () => false);
+ cy.visit("/verify?userId=221394658884845598&code=abc&invite=true");
+ cy.contains("Could not verify invite", { timeout: 10_000 });
+ });
+});
diff --git a/apps/login/cypress/integration/login.cy.ts b/apps/login/cypress/integration/login.cy.ts
index d4edf570d6..3e74c0f7fe 100644
--- a/apps/login/cypress/integration/login.cy.ts
+++ b/apps/login/cypress/integration/login.cy.ts
@@ -2,6 +2,14 @@ import { stub } from "../support/mock";
describe("login", () => {
beforeEach(() => {
+ stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [{ id: "256088834543534543" }],
+ },
+ });
stub("zitadel.session.v2.SessionService", "CreateSession", {
data: {
details: {
@@ -41,6 +49,7 @@ describe("login", () => {
data: {
settings: {
passkeysType: 1,
+ allowUsernamePassword: true,
},
},
});
@@ -104,16 +113,16 @@ describe("login", () => {
},
});
});
- it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => {
- cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
- cy.location("pathname", { timeout: 10_000 }).should("eq", "/password");
- cy.get('input[type="password"]').focus().type("MyStrongPassword!1");
- cy.get('button[type="submit"]').click();
- cy.location("pathname", { timeout: 10_000 }).should(
- "eq",
- "/passkey/add",
- );
- });
+ // it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => {
+ // cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
+ // cy.location("pathname", { timeout: 10_000 }).should("eq", "/password");
+ // cy.get('input[type="password"]').focus().type("MyStrongPassword!1");
+ // cy.get('button[type="submit"]').click();
+ // cy.location("pathname", { timeout: 10_000 }).should(
+ // "eq",
+ // "/passkey/set",
+ // );
+ // });
});
});
describe("passkey login", () => {
@@ -156,12 +165,10 @@ describe("login", () => {
},
});
});
- it("should redirect a user with passwordless authentication to /passkey/login", () => {
+
+ it("should redirect a user with passwordless authentication to /passkey", () => {
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
- cy.location("pathname", { timeout: 10_000 }).should(
- "eq",
- "/passkey/login",
- );
+ cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey");
});
});
});
diff --git a/apps/login/cypress/integration/register.cy.ts b/apps/login/cypress/integration/register.cy.ts
index 4cad0dbd81..262302c4c3 100644
--- a/apps/login/cypress/integration/register.cy.ts
+++ b/apps/login/cypress/integration/register.cy.ts
@@ -2,21 +2,75 @@ import { stub } from "../support/mock";
describe("register", () => {
beforeEach(() => {
+ stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [{ id: "256088834543534543" }],
+ },
+ });
+ stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
+ data: {
+ settings: {
+ passkeysType: 1,
+ allowRegister: true,
+ allowUsernamePassword: true,
+ defaultRedirectUri: "",
+ },
+ },
+ });
stub("zitadel.user.v2.UserService", "AddHumanUser", {
data: {
- userId: "123",
+ userId: "221394658884845598",
+ },
+ });
+ stub("zitadel.session.v2.SessionService", "CreateSession", {
+ data: {
+ details: {
+ sequence: 859,
+ changeDate: new Date("2024-04-04T09:40:55.577Z"),
+ resourceOwner: "220516472055706145",
+ },
+ sessionId: "221394658884845598",
+ sessionToken:
+ "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
+ challenges: undefined,
+ },
+ });
+
+ stub("zitadel.session.v2.SessionService", "GetSession", {
+ data: {
+ session: {
+ id: "221394658884845598",
+ creationDate: new Date("2024-04-04T09:40:55.577Z"),
+ changeDate: new Date("2024-04-04T09:40:55.577Z"),
+ sequence: 859,
+ factors: {
+ user: {
+ id: "221394658884845598",
+ loginName: "john@zitadel.com",
+ },
+ password: undefined,
+ webAuthN: undefined,
+ intent: undefined,
+ },
+ metadata: {},
+ },
},
});
});
- it("should redirect a user who selects passwordless on register to /passkeys/add", () => {
+ it("should redirect a user who selects passwordless on register to /passkey/set", () => {
cy.visit("/register");
- cy.get('input[autocomplete="firstname"]').focus().type("John");
- cy.get('input[autocomplete="lastname"]').focus().type("Doe");
- cy.get('input[autocomplete="email"]').focus().type("john@zitadel.com");
+ cy.get('input[data-testid="firstname-text-input"]').focus().type("John");
+ cy.get('input[data-testid="lastname-text-input"]').focus().type("Doe");
+ cy.get('input[data-testid="email-text-input"]')
+ .focus()
+ .type("john@zitadel.com");
cy.get('input[type="checkbox"][value="privacypolicy"]').check();
cy.get('input[type="checkbox"][value="tos"]').check();
cy.get('button[type="submit"]').click();
- cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey/add");
+ cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey/set");
});
});
diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts
index fbf9a7c39d..464bf02e59 100644
--- a/apps/login/cypress/integration/verify.cy.ts
+++ b/apps/login/cypress/integration/verify.cy.ts
@@ -1,19 +1,96 @@
import { stub } from "../support/mock";
-describe("/verify", () => {
- it("redirects after successful email verification", () => {
- stub("zitadel.user.v2.UserService", "VerifyEmail");
- cy.visit("/verify?userId=123&code=abc&submit=true");
- cy.location("pathname", { timeout: 10_000 }).should("eq", "/loginname");
+describe("verify email", () => {
+ beforeEach(() => {
+ stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [{ id: "256088834543534543" }],
+ },
+ });
+
+ stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
+ data: {
+ authMethodTypes: [1], // set one method such that we know that the user was not invited
+ },
+ });
+
+ stub("zitadel.user.v2.UserService", "SendEmailCode");
+
+ stub("zitadel.user.v2.UserService", "GetUserByID", {
+ data: {
+ user: {
+ userId: "221394658884845598",
+ state: 1,
+ username: "john@zitadel.com",
+ loginNames: ["john@zitadel.com"],
+ preferredLoginName: "john@zitadel.com",
+ human: {
+ userId: "221394658884845598",
+ state: 1,
+ username: "john@zitadel.com",
+ loginNames: ["john@zitadel.com"],
+ preferredLoginName: "john@zitadel.com",
+ profile: {
+ givenName: "John",
+ familyName: "Doe",
+ avatarUrl: "https://zitadel.com/avatar.jpg",
+ },
+ email: {
+ email: "john@zitadel.com",
+ isVerified: false, // email is not verified yet
+ },
+ },
+ },
+ },
+ });
+
+ stub("zitadel.session.v2.SessionService", "CreateSession", {
+ data: {
+ details: {
+ sequence: 859,
+ changeDate: new Date("2024-04-04T09:40:55.577Z"),
+ resourceOwner: "220516472055706145",
+ },
+ sessionId: "221394658884845598",
+ sessionToken:
+ "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
+ challenges: undefined,
+ },
+ });
+
+ stub("zitadel.session.v2.SessionService", "GetSession", {
+ data: {
+ session: {
+ id: "221394658884845598",
+ creationDate: new Date("2024-04-04T09:40:55.577Z"),
+ changeDate: new Date("2024-04-04T09:40:55.577Z"),
+ sequence: 859,
+ factors: {
+ user: {
+ id: "221394658884845598",
+ loginName: "john@zitadel.com",
+ },
+ password: undefined,
+ webAuthN: undefined,
+ intent: undefined,
+ },
+ metadata: {},
+ },
+ },
+ });
});
- it("shows an error if validation failed", () => {
+
+ it("shows an error if email code validation failed", () => {
stub("zitadel.user.v2.UserService", "VerifyEmail", {
code: 3,
error: "error validating code",
});
// TODO: Avoid uncaught exception in application
cy.once("uncaught:exception", () => false);
- cy.visit("/verify?userId=123&code=abc&submit=true");
- cy.contains("error validating code");
+ cy.visit("/verify?userId=221394658884845598&code=abc");
+ cy.contains("Could not verify email", { timeout: 10_000 });
});
});
diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json
new file mode 100644
index 0000000000..5e8756c89b
--- /dev/null
+++ b/apps/login/locales/de.json
@@ -0,0 +1,195 @@
+{
+ "common": {
+ "back": "Zurück"
+ },
+ "accounts": {
+ "title": "Konten",
+ "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.",
+ "addAnother": "Ein weiteres Konto hinzufügen",
+ "noResults": "Keine Konten gefunden"
+ },
+ "loginname": {
+ "title": "Willkommen zurück!",
+ "description": "Geben Sie Ihre Anmeldedaten ein.",
+ "register": "Neuen Benutzer registrieren"
+ },
+ "password": {
+ "verify": {
+ "title": "Passwort",
+ "description": "Geben Sie Ihr Passwort ein.",
+ "resetPassword": "Passwort zurücksetzen",
+ "submit": "Weiter"
+ },
+ "set": {
+ "title": "Passwort festlegen",
+ "description": "Legen Sie das Passwort für Ihr Konto fest",
+ "codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.",
+ "noCodeReceived": "Keinen Code erhalten?",
+ "resend": "Erneut senden",
+ "submit": "Weiter"
+ },
+ "change": {
+ "title": "Passwort ändern",
+ "description": "Legen Sie das Passwort für Ihr Konto fest",
+ "submit": "Weiter"
+ }
+ },
+ "idp": {
+ "title": "Mit SSO anmelden",
+ "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden",
+ "signInWithApple": "Mit Apple anmelden",
+ "signInWithGoogle": "Mit Google anmelden",
+ "signInWithAzureAD": "Mit AzureAD anmelden",
+ "signInWithGithub": "Mit GitHub anmelden",
+ "signInWithGitlab": "Mit GitLab anmelden",
+ "loginSuccess": {
+ "title": "Anmeldung erfolgreich",
+ "description": "Sie haben sich erfolgreich angemeldet!"
+ },
+ "linkingSuccess": {
+ "title": "Konto verknüpft",
+ "description": "Sie haben Ihr Konto erfolgreich verknüpft!"
+ },
+ "registerSuccess": {
+ "title": "Registrierung erfolgreich",
+ "description": "Sie haben sich erfolgreich registriert!"
+ },
+ "loginError": {
+ "title": "Anmeldung fehlgeschlagen",
+ "description": "Beim Anmelden ist ein Fehler aufgetreten."
+ },
+ "linkingError": {
+ "title": "Konto-Verknüpfung fehlgeschlagen",
+ "description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten."
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "Bestätigen Sie Ihre Identität",
+ "description": "Wählen Sie einen der folgenden Faktoren.",
+ "noResults": "Keine zweiten Faktoren verfügbar, um sie einzurichten."
+ },
+ "set": {
+ "title": "2-Faktor einrichten",
+ "description": "Wählen Sie einen der folgenden zweiten Faktoren."
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "2-Faktor bestätigen",
+ "totpDescription": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein.",
+ "smsDescription": "Geben Sie den Code ein, den Sie per SMS erhalten haben.",
+ "emailDescription": "Geben Sie den Code ein, den Sie per E-Mail erhalten haben.",
+ "noCodeReceived": "Keinen Code erhalten?",
+ "resendCode": "Code erneut senden",
+ "submit": "Weiter"
+ },
+ "set": {
+ "title": "2-Faktor einrichten",
+ "totpDescription": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App.",
+ "smsDescription": "Geben Sie Ihre Telefonnummer ein, um einen Code per SMS zu erhalten.",
+ "emailDescription": "Geben Sie Ihre E-Mail-Adresse ein, um einen Code per E-Mail zu erhalten.",
+ "totpRegisterDescription": "Scannen Sie den QR-Code oder navigieren Sie manuell zur URL.",
+ "submit": "Weiter"
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "Mit einem Passkey authentifizieren",
+ "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen",
+ "usePassword": "Passwort verwenden",
+ "submit": "Weiter"
+ },
+ "set": {
+ "title": "Passkey einrichten",
+ "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen",
+ "info": {
+ "description": "Ein Passkey ist eine Authentifizierungsmethode auf einem Gerät wie Ihr Fingerabdruck, Apple FaceID oder ähnliches.",
+ "link": "Passwortlose Authentifizierung"
+ },
+ "skip": "Überspringen",
+ "submit": "Weiter"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "2-Faktor bestätigen",
+ "description": "Bestätigen Sie Ihr Konto mit Ihrem Gerät."
+ },
+ "set": {
+ "title": "2-Faktor einrichten",
+ "description": "Richten Sie ein Gerät als zweiten Faktor ein.",
+ "submit": "Weiter"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "Passkey",
+ "password": "Password"
+ },
+ "disabled": {
+ "title": "Registrierung deaktiviert",
+ "description": "Die Registrierung ist deaktiviert. Bitte wenden Sie sich an den Administrator."
+ },
+ "missingdata": {
+ "title": "Registrierung fehlgeschlagen",
+ "description": "Einige Daten fehlen. Bitte überprüfen Sie Ihre Eingaben."
+ },
+ "title": "Registrieren",
+ "description": "Erstellen Sie Ihr ZITADEL-Konto.",
+ "selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten",
+ "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen",
+ "termsOfService": "Nutzungsbedingungen",
+ "privacyPolicy": "Datenschutzrichtlinie",
+ "submit": "Weiter",
+ "password": {
+ "title": "Passwort festlegen",
+ "description": "Legen Sie das Passwort für Ihr Konto fest",
+ "submit": "Weiter"
+ }
+ },
+ "invite": {
+ "title": "Benutzer einladen",
+ "description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.",
+ "info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.",
+ "notAllowed": "Sie haben keine Berechtigung, Benutzer einzuladen.",
+ "submit": "Einladen",
+ "success": {
+ "title": "Einladung erfolgreich",
+ "description": "Der Benutzer wurde erfolgreich eingeladen.",
+ "verified": "Der Benutzer wurde eingeladen und hat seine E-Mail bereits verifiziert.",
+ "notVerifiedYet": "Der Benutzer wurde eingeladen. Er erhält eine E-Mail mit weiteren Anweisungen.",
+ "submit": "Weiteren Benutzer einladen"
+ }
+ },
+ "signedin": {
+ "title": "Willkommen {user}!",
+ "description": "Sie sind angemeldet.",
+ "continue": "Weiter"
+ },
+ "verify": {
+ "userIdMissing": "Keine Benutzer-ID angegeben!",
+ "success": "Erfolgreich verifiziert",
+ "setupAuthenticator": "Authentifikator einrichten",
+ "verify": {
+ "title": "Benutzer verifizieren",
+ "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
+ "noCodeReceived": "Keinen Code erhalten?",
+ "resendCode": "Code erneut senden",
+ "submit": "Weiter"
+ }
+ },
+ "authenticator": {
+ "title": "Authentifizierungsmethode auswählen",
+ "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.",
+ "noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar",
+ "allSetup": "Sie haben bereits einen Authentifikator eingerichtet!",
+ "linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter"
+ },
+ "error": {
+ "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",
+ "sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
+ "failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
+ "tryagain": "Erneut versuchen"
+ }
+}
diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json
new file mode 100644
index 0000000000..3101f222d5
--- /dev/null
+++ b/apps/login/locales/en.json
@@ -0,0 +1,195 @@
+{
+ "common": {
+ "back": "Back"
+ },
+ "accounts": {
+ "title": "Accounts",
+ "description": "Select the account you want to use.",
+ "addAnother": "Add another account",
+ "noResults": "No accounts found"
+ },
+ "loginname": {
+ "title": "Welcome back!",
+ "description": "Enter your login data.",
+ "register": "Register new user"
+ },
+ "password": {
+ "verify": {
+ "title": "Password",
+ "description": "Enter your password.",
+ "resetPassword": "Reset Password",
+ "submit": "Continue"
+ },
+ "set": {
+ "title": "Set Password",
+ "description": "Set the password for your account",
+ "codeSent": "A code has been sent to your email address.",
+ "noCodeReceived": "Didn't receive a code?",
+ "resend": "Resend code",
+ "submit": "Continue"
+ },
+ "change": {
+ "title": "Change Password",
+ "description": "Set the password for your account",
+ "submit": "Continue"
+ }
+ },
+ "idp": {
+ "title": "Sign in with SSO",
+ "description": "Select one of the following providers to sign in",
+ "signInWithApple": "Sign in with Apple",
+ "signInWithGoogle": "Sign in with Google",
+ "signInWithAzureAD": "Sign in with AzureAD",
+ "signInWithGithub": "Sign in with GitHub",
+ "signInWithGitlab": "Sign in with GitLab",
+ "loginSuccess": {
+ "title": "Login successful",
+ "description": "You have successfully been loggedIn!"
+ },
+ "linkingSuccess": {
+ "title": "Account linked",
+ "description": "You have successfully linked your account!"
+ },
+ "registerSuccess": {
+ "title": "Registration successful",
+ "description": "You have successfully registered!"
+ },
+ "loginError": {
+ "title": "Login failed",
+ "description": "An error occurred while trying to login."
+ },
+ "linkingError": {
+ "title": "Account linking failed",
+ "description": "An error occurred while trying to link your account."
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "Verify your identity",
+ "description": "Choose one of the following factors.",
+ "noResults": "No second factors available to setup."
+ },
+ "set": {
+ "title": "Set up 2-Factor",
+ "description": "Choose one of the following second factors."
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "Verify 2-Factor",
+ "totpDescription": "Enter the code from your authenticator app.",
+ "smsDescription": "Enter the code you received via SMS.",
+ "emailDescription": "Enter the code you received via email.",
+ "noCodeReceived": "Didn't receive a code?",
+ "resendCode": "Resend code",
+ "submit": "Continue"
+ },
+ "set": {
+ "title": "Set up 2-Factor",
+ "totpDescription": "Scan the QR code with your authenticator app.",
+ "smsDescription": "Enter your phone number to receive a code via SMS.",
+ "emailDescription": "Enter your email address to receive a code via email.",
+ "totpRegisterDescription": "Scan the QR Code or navigate to the URL manually.",
+ "submit": "Continue"
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "Authenticate with a passkey",
+ "description": "Your device will ask for your fingerprint, face, or screen lock",
+ "usePassword": "Use password",
+ "submit": "Continue"
+ },
+ "set": {
+ "title": "Setup a passkey",
+ "description": "Your device will ask for your fingerprint, face, or screen lock",
+ "info": {
+ "description": "A passkey is an authentication method on a device like your fingerprint, Apple FaceID or similar. ",
+ "link": "Passwordless Authentication"
+ },
+ "skip": "Skip",
+ "submit": "Continue"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "Verify 2-Factor",
+ "description": "Verify your account with your device."
+ },
+ "set": {
+ "title": "Set up 2-Factor",
+ "description": "Set up a device as a second factor.",
+ "submit": "Continue"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "Passkey",
+ "password": "Password"
+ },
+ "disabled": {
+ "title": "Registration disabled",
+ "description": "The registration is disabled. Please contact your administrator."
+ },
+ "missingdata": {
+ "title": "Missing data",
+ "description": "Provide email, first and last name to register."
+ },
+ "title": "Register",
+ "description": "Create your ZITADEL account.",
+ "selectMethod": "Select the method you would like to authenticate",
+ "agreeTo": "To register you must agree to the terms and conditions",
+ "termsOfService": "Terms of Service",
+ "privacyPolicy": "Privacy Policy",
+ "submit": "Continue",
+ "password": {
+ "title": "Set Password",
+ "description": "Set the password for your account",
+ "submit": "Continue"
+ }
+ },
+ "invite": {
+ "title": "Invite User",
+ "description": "Provide the email address and the name of the user you want to invite.",
+ "info": "The user will receive an email with further instructions.",
+ "notAllowed": "Your settings do not allow you to invite users.",
+ "submit": "Continue",
+ "success": {
+ "title": "User invited",
+ "description": "The email has successfully been sent.",
+ "verified": "The user has been invited and has already verified his email.",
+ "notVerifiedYet": "The user has been invited. They will receive an email with further instructions.",
+ "submit": "Invite another user"
+ }
+ },
+ "signedin": {
+ "title": "Welcome {user}!",
+ "description": "You are signed in.",
+ "continue": "Continue"
+ },
+ "verify": {
+ "userIdMissing": "No userId provided!",
+ "success": "The user has been verified successfully.",
+ "setupAuthenticator": "Setup authenticator",
+ "verify": {
+ "title": "Verify user",
+ "description": "Enter the Code provided in the verification email.",
+ "noCodeReceived": "Didn't receive a code?",
+ "resendCode": "Resend code",
+ "submit": "Continue"
+ }
+ },
+ "authenticator": {
+ "title": "Choose authentication method",
+ "description": "Select the method you would like to authenticate",
+ "noMethodsAvailable": "No authentication methods available",
+ "allSetup": "You have already setup an authenticator!",
+ "linkWithIDP": "or link with an Identity Provider"
+ },
+ "error": {
+ "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",
+ "sessionExpired": "Your current session has expired. Please login again.",
+ "failedLoading": "Failed to load data. Please try again.",
+ "tryagain": "Try Again"
+ }
+}
diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json
new file mode 100644
index 0000000000..5a9b6f6324
--- /dev/null
+++ b/apps/login/locales/es.json
@@ -0,0 +1,195 @@
+{
+ "common": {
+ "back": "Atrás"
+ },
+ "accounts": {
+ "title": "Cuentas",
+ "description": "Selecciona la cuenta que deseas usar.",
+ "addAnother": "Agregar otra cuenta",
+ "noResults": "No se encontraron cuentas"
+ },
+ "loginname": {
+ "title": "¡Bienvenido de nuevo!",
+ "description": "Introduce tus datos de acceso.",
+ "register": "Registrar nuevo usuario"
+ },
+ "password": {
+ "verify": {
+ "title": "Contraseña",
+ "description": "Introduce tu contraseña.",
+ "resetPassword": "Restablecer contraseña",
+ "submit": "Continuar"
+ },
+ "set": {
+ "title": "Establecer Contraseña",
+ "description": "Establece la contraseña para tu cuenta",
+ "codeSent": "Se ha enviado un código a su correo electrónico.",
+ "noCodeReceived": "¿No recibiste un código?",
+ "resend": "Reenviar código",
+ "submit": "Continuar"
+ },
+ "change": {
+ "title": "Cambiar Contraseña",
+ "description": "Establece la contraseña para tu cuenta",
+ "submit": "Continuar"
+ }
+ },
+ "idp": {
+ "title": "Iniciar sesión con SSO",
+ "description": "Selecciona uno de los siguientes proveedores para iniciar sesión",
+ "signInWithApple": "Iniciar sesión con Apple",
+ "signInWithGoogle": "Iniciar sesión con Google",
+ "signInWithAzureAD": "Iniciar sesión con AzureAD",
+ "signInWithGithub": "Iniciar sesión con GitHub",
+ "signInWithGitlab": "Iniciar sesión con GitLab",
+ "loginSuccess": {
+ "title": "Inicio de sesión exitoso",
+ "description": "¡Has iniciado sesión con éxito!"
+ },
+ "linkingSuccess": {
+ "title": "Cuenta vinculada",
+ "description": "¡Has vinculado tu cuenta con éxito!"
+ },
+ "registerSuccess": {
+ "title": "Registro exitoso",
+ "description": "¡Te has registrado con éxito!"
+ },
+ "loginError": {
+ "title": "Error de inicio de sesión",
+ "description": "Ocurrió un error al intentar iniciar sesión."
+ },
+ "linkingError": {
+ "title": "Error al vincular la cuenta",
+ "description": "Ocurrió un error al intentar vincular tu cuenta."
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "Verifica tu identidad",
+ "description": "Elige uno de los siguientes factores.",
+ "noResults": "No hay factores secundarios disponibles para configurar."
+ },
+ "set": {
+ "title": "Configurar autenticación de 2 factores",
+ "description": "Elige uno de los siguientes factores secundarios."
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "Verificar autenticación de 2 factores",
+ "totpDescription": "Introduce el código de tu aplicación de autenticación.",
+ "smsDescription": "Introduce el código que recibiste por SMS.",
+ "emailDescription": "Introduce el código que recibiste por correo electrónico.",
+ "noCodeReceived": "¿No recibiste un código?",
+ "resendCode": "Reenviar código",
+ "submit": "Continuar"
+ },
+ "set": {
+ "title": "Configurar autenticación de 2 factores",
+ "totpDescription": "Escanea el código QR con tu aplicación de autenticación.",
+ "smsDescription": "Introduce tu número de teléfono para recibir un código por SMS.",
+ "emailDescription": "Introduce tu dirección de correo electrónico para recibir un código por correo electrónico.",
+ "totpRegisterDescription": "Escanea el código QR o navega manualmente a la URL.",
+ "submit": "Continuar"
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "Autenticar con una clave de acceso",
+ "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla",
+ "usePassword": "Usar contraseña",
+ "submit": "Continuar"
+ },
+ "set": {
+ "title": "Configurar una clave de acceso",
+ "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla",
+ "info": {
+ "description": "Una clave de acceso es un método de autenticación en un dispositivo como tu huella digital, Apple FaceID o similar.",
+ "link": "Autenticación sin contraseña"
+ },
+ "skip": "Omitir",
+ "submit": "Continuar"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "Verificar autenticación de 2 factores",
+ "description": "Verifica tu cuenta con tu dispositivo."
+ },
+ "set": {
+ "title": "Configurar autenticación de 2 factores",
+ "description": "Configura un dispositivo como segundo factor.",
+ "submit": "Continuar"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "Clave de acceso",
+ "password": "Contraseña"
+ },
+ "disabled": {
+ "title": "Registro deshabilitado",
+ "description": "Registrarse está deshabilitado en este momento."
+ },
+ "missingdata": {
+ "title": "Datos faltantes",
+ "description": "No se proporcionaron datos suficientes para el registro."
+ },
+ "title": "Registrarse",
+ "description": "Crea tu cuenta ZITADEL.",
+ "selectMethod": "Selecciona el método con el que deseas autenticarte",
+ "agreeTo": "Para registrarte debes aceptar los términos y condiciones",
+ "termsOfService": "Términos de Servicio",
+ "privacyPolicy": "Política de Privacidad",
+ "submit": "Continuar",
+ "password": {
+ "title": "Establecer Contraseña",
+ "description": "Establece la contraseña para tu cuenta",
+ "submit": "Continuar"
+ }
+ },
+ "invite": {
+ "title": "Invitar usuario",
+ "description": "Introduce el correo electrónico del usuario que deseas invitar.",
+ "info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.",
+ "notAllowed": "No tienes permiso para invitar usuarios.",
+ "submit": "Invitar usuario",
+ "success": {
+ "title": "¡Usuario invitado!",
+ "description": "El usuario ha sido invitado.",
+ "verified": "El usuario ha sido invitado y ya ha verificado su correo electrónico.",
+ "notVerifiedYet": "El usuario ha sido invitado. Recibirá un correo electrónico con más instrucciones.",
+ "submit": "Invitar a otro usuario"
+ }
+ },
+ "signedin": {
+ "title": "¡Bienvenido {user}!",
+ "description": "Has iniciado sesión.",
+ "continue": "Continuar"
+ },
+ "verify": {
+ "userIdMissing": "¡No se proporcionó userId!",
+ "success": "¡Verificación exitosa!",
+ "setupAuthenticator": "Configurar autenticador",
+ "verify": {
+ "title": "Verificar usuario",
+ "description": "Introduce el código proporcionado en el correo electrónico de verificación.",
+ "noCodeReceived": "¿No recibiste un código?",
+ "resendCode": "Reenviar código",
+ "submit": "Continuar"
+ }
+ },
+ "authenticator": {
+ "title": "Seleccionar método de autenticación",
+ "description": "Selecciona el método con el que deseas autenticarte",
+ "noMethodsAvailable": "No hay métodos de autenticación disponibles",
+ "allSetup": "¡Ya has configurado un autenticador!",
+ "linkWithIDP": "o vincúlalo con un proveedor de identidad"
+ },
+ "error": {
+ "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",
+ "sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.",
+ "failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.",
+ "tryagain": "Intentar de nuevo"
+ }
+}
diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json
new file mode 100644
index 0000000000..1423c43cfe
--- /dev/null
+++ b/apps/login/locales/it.json
@@ -0,0 +1,195 @@
+{
+ "common": {
+ "back": "Indietro"
+ },
+ "accounts": {
+ "title": "Account",
+ "description": "Seleziona l'account che desideri utilizzare.",
+ "addAnother": "Aggiungi un altro account",
+ "noResults": "Nessun account trovato"
+ },
+ "loginname": {
+ "title": "Bentornato!",
+ "description": "Inserisci i tuoi dati di accesso.",
+ "register": "Registrati come nuovo utente"
+ },
+ "password": {
+ "verify": {
+ "title": "Password",
+ "description": "Inserisci la tua password.",
+ "resetPassword": "Reimposta Password",
+ "submit": "Continua"
+ },
+ "set": {
+ "title": "Imposta Password",
+ "description": "Imposta la password per il tuo account",
+ "codeSent": "Un codice è stato inviato al tuo indirizzo email.",
+ "noCodeReceived": "Non hai ricevuto un codice?",
+ "resend": "Invia di nuovo",
+ "submit": "Continua"
+ },
+ "change": {
+ "title": "Cambia Password",
+ "description": "Imposta la password per il tuo account",
+ "submit": "Continua"
+ }
+ },
+ "idp": {
+ "title": "Accedi con SSO",
+ "description": "Seleziona uno dei seguenti provider per accedere",
+ "signInWithApple": "Accedi con Apple",
+ "signInWithGoogle": "Accedi con Google",
+ "signInWithAzureAD": "Accedi con AzureAD",
+ "signInWithGithub": "Accedi con GitHub",
+ "signInWithGitlab": "Accedi con GitLab",
+ "loginSuccess": {
+ "title": "Accesso riuscito",
+ "description": "Accesso effettuato con successo!"
+ },
+ "linkingSuccess": {
+ "title": "Account collegato",
+ "description": "Hai collegato con successo il tuo account!"
+ },
+ "registerSuccess": {
+ "title": "Registrazione riuscita",
+ "description": "Registrazione effettuata con successo!"
+ },
+ "loginError": {
+ "title": "Accesso fallito",
+ "description": "Si è verificato un errore durante il tentativo di accesso."
+ },
+ "linkingError": {
+ "title": "Collegamento account fallito",
+ "description": "Si è verificato un errore durante il tentativo di collegare il tuo account."
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "Verifica la tua identità",
+ "description": "Scegli uno dei seguenti fattori.",
+ "noResults": "Nessun secondo fattore disponibile per la configurazione."
+ },
+ "set": {
+ "title": "Configura l'autenticazione a 2 fattori",
+ "description": "Scegli uno dei seguenti secondi fattori."
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "Verifica l'autenticazione a 2 fattori",
+ "totpDescription": "Inserisci il codice dalla tua app di autenticazione.",
+ "smsDescription": "Inserisci il codice ricevuto via SMS.",
+ "emailDescription": "Inserisci il codice ricevuto via email.",
+ "noCodeReceived": "Non hai ricevuto un codice?",
+ "resendCode": "Invia di nuovo il codice",
+ "submit": "Continua"
+ },
+ "set": {
+ "title": "Configura l'autenticazione a 2 fattori",
+ "totpDescription": "Scansiona il codice QR con la tua app di autenticazione.",
+ "smsDescription": "Inserisci il tuo numero di telefono per ricevere un codice via SMS.",
+ "emailDescription": "Inserisci il tuo indirizzo email per ricevere un codice via email.",
+ "totpRegisterDescription": "Scansiona il codice QR o naviga manualmente all'URL.",
+ "submit": "Continua"
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "Autenticati con una passkey",
+ "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo",
+ "usePassword": "Usa password",
+ "submit": "Continua"
+ },
+ "set": {
+ "title": "Configura una passkey",
+ "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo",
+ "info": {
+ "description": "Una passkey è un metodo di autenticazione su un dispositivo come la tua impronta digitale, Apple FaceID o simili.",
+ "link": "Autenticazione senza password"
+ },
+ "skip": "Salta",
+ "submit": "Continua"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "Verifica l'autenticazione a 2 fattori",
+ "description": "Verifica il tuo account con il tuo dispositivo."
+ },
+ "set": {
+ "title": "Configura l'autenticazione a 2 fattori",
+ "description": "Configura un dispositivo come secondo fattore.",
+ "submit": "Continua"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "Passkey",
+ "password": "Password"
+ },
+ "disabled": {
+ "title": "Registration disabled",
+ "description": "Registrazione disabilitata. Contatta l'amministratore di sistema per assistenza."
+ },
+ "missingdata": {
+ "title": "Registrazione",
+ "description": "Inserisci i tuoi dati per registrarti."
+ },
+ "title": "Registrati",
+ "description": "Crea il tuo account ZITADEL.",
+ "selectMethod": "Seleziona il metodo con cui desideri autenticarti",
+ "agreeTo": "Per registrarti devi accettare i termini e le condizioni",
+ "termsOfService": "Termini di Servizio",
+ "privacyPolicy": "Informativa sulla Privacy",
+ "submit": "Continua",
+ "password": {
+ "title": "Imposta Password",
+ "description": "Imposta la password per il tuo account",
+ "submit": "Continua"
+ }
+ },
+ "invite": {
+ "title": "Invita Utente",
+ "description": "Inserisci l'indirizzo email dell'utente che desideri invitare.",
+ "info": "L'utente riceverà un'email con ulteriori istruzioni.",
+ "notAllowed": "Non hai i permessi per invitare un utente.",
+ "submit": "Invita Utente",
+ "success": {
+ "title": "Invito inviato",
+ "description": "L'utente è stato invitato con successo.",
+ "verified": "L'utente è stato invitato e ha già verificato la sua email.",
+ "notVerifiedYet": "L'utente è stato invitato. Riceverà un'email con ulteriori istruzioni.",
+ "submit": "Invita un altro utente"
+ }
+ },
+ "signedin": {
+ "title": "Benvenuto {user}!",
+ "description": "Sei connesso.",
+ "continue": "Continua"
+ },
+ "verify": {
+ "userIdMissing": "Nessun userId fornito!",
+ "success": "Verifica effettuata con successo!",
+ "setupAuthenticator": "Configura autenticatore",
+ "verify": {
+ "title": "Verifica utente",
+ "description": "Inserisci il codice fornito nell'email di verifica.",
+ "noCodeReceived": "Non hai ricevuto un codice?",
+ "resendCode": "Invia di nuovo il codice",
+ "submit": "Continua"
+ }
+ },
+ "authenticator": {
+ "title": "Seleziona metodo di autenticazione",
+ "description": "Seleziona il metodo con cui desideri autenticarti",
+ "noMethodsAvailable": "Nessun metodo di autenticazione disponibile",
+ "allSetup": "Hai già configurato un autenticatore!",
+ "linkWithIDP": "o collega con un Identity Provider"
+ },
+ "error": {
+ "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",
+ "sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.",
+ "failedLoading": "Impossibile caricare i dati. Riprova.",
+ "tryagain": "Riprova"
+ }
+}
diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json
new file mode 100644
index 0000000000..acd03cc5b6
--- /dev/null
+++ b/apps/login/locales/zh.json
@@ -0,0 +1,195 @@
+{
+ "common": {
+ "back": "返回"
+ },
+ "accounts": {
+ "title": "账户",
+ "description": "选择您想使用的账户。",
+ "addAnother": "添加另一个账户",
+ "noResults": "未找到账户"
+ },
+ "loginname": {
+ "title": "欢迎回来!",
+ "description": "请输入您的登录信息。",
+ "register": "注册新用户"
+ },
+ "password": {
+ "verify": {
+ "title": "密码",
+ "description": "请输入您的密码。",
+ "resetPassword": "重置密码",
+ "submit": "继续"
+ },
+ "set": {
+ "title": "设置密码",
+ "description": "为您的账户设置密码",
+ "codeSent": "验证码已发送到您的邮箱。",
+ "noCodeReceived": "没有收到验证码?",
+ "resend": "重发验证码",
+ "submit": "继续"
+ },
+ "change": {
+ "title": "更改密码",
+ "description": "为您的账户设置密码",
+ "submit": "继续"
+ }
+ },
+ "idp": {
+ "title": "使用 SSO 登录",
+ "description": "选择以下提供商中的一个进行登录",
+ "signInWithApple": "用 Apple 登录",
+ "signInWithGoogle": "用 Google 登录",
+ "signInWithAzureAD": "用 AzureAD 登录",
+ "signInWithGithub": "用 GitHub 登录",
+ "signInWithGitlab": "用 GitLab 登录",
+ "loginSuccess": {
+ "title": "登录成功",
+ "description": "您已成功登录!"
+ },
+ "linkingSuccess": {
+ "title": "账户已链接",
+ "description": "您已成功链接您的账户!"
+ },
+ "registerSuccess": {
+ "title": "注册成功",
+ "description": "您已成功注册!"
+ },
+ "loginError": {
+ "title": "登录失败",
+ "description": "登录时发生错误。"
+ },
+ "linkingError": {
+ "title": "账户链接失败",
+ "description": "链接账户时发生错误。"
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "验证您的身份",
+ "description": "选择以下的一个因素。",
+ "noResults": "没有可设置的第二因素。"
+ },
+ "set": {
+ "title": "设置双因素认证",
+ "description": "选择以下的一个第二因素。"
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "验证双因素",
+ "totpDescription": "请输入认证应用程序中的验证码。",
+ "smsDescription": "输入通过短信收到的验证码。",
+ "emailDescription": "输入通过电子邮件收到的验证码。",
+ "noCodeReceived": "没有收到验证码?",
+ "resendCode": "重发验证码",
+ "submit": "继续"
+ },
+ "set": {
+ "title": "设置双因素认证",
+ "totpDescription": "使用认证应用程序扫描二维码。",
+ "smsDescription": "输入您的电话号码以接收短信验证码。",
+ "emailDescription": "输入您的电子邮箱地址以接收电子邮件验证码。",
+ "totpRegisterDescription": "扫描二维码或手动导航到URL。",
+ "submit": "继续"
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "使用密钥认证",
+ "description": "您的设备将请求指纹、面部识别或屏幕锁",
+ "usePassword": "使用密码",
+ "submit": "继续"
+ },
+ "set": {
+ "title": "设置密钥",
+ "description": "您的设备将请求指纹、面部识别或屏幕锁",
+ "info": {
+ "description": "密钥是在设备上如指纹、Apple FaceID 或类似的认证方法。",
+ "link": "无密码认证"
+ },
+ "skip": "跳过",
+ "submit": "继续"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "验证双因素",
+ "description": "使用您的设备验证帐户。"
+ },
+ "set": {
+ "title": "设置双因素认证",
+ "description": "设置设备为第二因素。",
+ "submit": "继续"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "密钥",
+ "password": "密码"
+ },
+ "disabled": {
+ "title": "注册已禁用",
+ "description": "您的设置不允许注册新用户。"
+ },
+ "missingdata": {
+ "title": "缺少数据",
+ "description": "请提供所有必需的数据。"
+ },
+ "title": "注册",
+ "description": "创建您的 ZITADEL 账户。",
+ "selectMethod": "选择您想使用的认证方法",
+ "agreeTo": "注册即表示您同意条款和条件",
+ "termsOfService": "服务条款",
+ "privacyPolicy": "隐私政策",
+ "submit": "继续",
+ "password": {
+ "title": "设置密码",
+ "description": "为您的账户设置密码",
+ "submit": "继续"
+ }
+ },
+ "invite": {
+ "title": "邀请用户",
+ "description": "提供您想邀请的用户的电子邮箱地址和姓名。",
+ "info": "用户将收到一封包含进一步说明的电子邮件。",
+ "notAllowed": "您的设置不允许邀请用户。",
+ "submit": "继续",
+ "success": {
+ "title": "用户已邀请",
+ "description": "邮件已成功发送。",
+ "verified": "用户已被邀请并已验证其电子邮件。",
+ "notVerifiedYet": "用户已被邀请。他们将收到一封包含进一步说明的电子邮件。",
+ "submit": "邀请另一位用户"
+ }
+ },
+ "signedin": {
+ "title": "欢迎 {user}!",
+ "description": "您已登录。",
+ "continue": "继续"
+ },
+ "verify": {
+ "userIdMissing": "未提供用户 ID!",
+ "success": "用户验证成功。",
+ "setupAuthenticator": "设置认证器",
+ "verify": {
+ "title": "验证用户",
+ "description": "输入验证邮件中的验证码。",
+ "noCodeReceived": "没有收到验证码?",
+ "resendCode": "重发验证码",
+ "submit": "继续"
+ }
+ },
+ "authenticator": {
+ "title": "选择认证方式",
+ "description": "选择您想使用的认证方法",
+ "noMethodsAvailable": "没有可用的认证方法",
+ "allSetup": "您已经设置好了一个认证器!",
+ "linkWithIDP": "或将其与身份提供者关联"
+ },
+ "error": {
+ "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。",
+ "sessionExpired": "当前会话已过期,请重新登录。",
+ "failedLoading": "加载数据失败,请再试一次。",
+ "tryagain": "重试"
+ }
+}
diff --git a/apps/login/mock/initial-stubs/zitadel.settings.v2beta.SettingsService.json b/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json
similarity index 96%
rename from apps/login/mock/initial-stubs/zitadel.settings.v2beta.SettingsService.json
rename to apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json
index 6ddf5989ed..07e9980f9b 100644
--- a/apps/login/mock/initial-stubs/zitadel.settings.v2beta.SettingsService.json
+++ b/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json
@@ -2,7 +2,9 @@
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetBrandingSettings",
- "out": {}
+ "out": {
+ "data": {}
+ }
},
{
"service": "zitadel.settings.v2.SettingsService",
diff --git a/apps/login/mock/mocked-services.cfg b/apps/login/mock/mocked-services.cfg
index 72e4d98748..6a758ab8c1 100644
--- a/apps/login/mock/mocked-services.cfg
+++ b/apps/login/mock/mocked-services.cfg
@@ -1,4 +1,5 @@
zitadel/user/v2/user_service.proto
+zitadel/org/v2/org_service.proto
zitadel/session/v2/session_service.proto
zitadel/settings/v2/settings_service.proto
zitadel/management.proto
diff --git a/apps/login/next-env.d.ts b/apps/login/next-env.d.ts
index 4f11a03dc6..1b3be0840f 100755
--- a/apps/login/next-env.d.ts
+++ b/apps/login/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs
index 4e71072736..a6e1d1a6d8 100755
--- a/apps/login/next.config.mjs
+++ b/apps/login/next.config.mjs
@@ -1,3 +1,7 @@
+import createNextIntlPlugin from "next-intl/plugin";
+
+const withNextIntl = createNextIntlPlugin();
+
/** @type {import('next').NextConfig} */
const secureHeaders = [
@@ -32,9 +36,8 @@ const secureHeaders = [
const nextConfig = {
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
- swcMinify: true,
experimental: {
- serverActions: true,
+ dynamicIO: true,
},
images: {
remotePatterns: [
@@ -62,4 +65,4 @@ const nextConfig = {
},
};
-export default nextConfig;
+export default withNextIntl(nextConfig);
diff --git a/apps/login/package.json b/apps/login/package.json
index aca25b506c..c4031458ca 100644
--- a/apps/login/package.json
+++ b/apps/login/package.json
@@ -1,14 +1,16 @@
{
"name": "@zitadel/login",
"private": true,
+ "type": "module",
"scripts": {
- "dev": "next dev",
+ "dev": "next dev --turbopack",
"test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'",
"test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'",
"test:unit": "vitest",
"test:unit:watch": "pnpm test:unit --watch",
"test:integration": "pnpm mock:build && concurrently --names 'mock,test' --success command-test --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test start http://localhost:3000 \"test:integration:run\"'",
- "test:integration:watch": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:run\\\"\"'",
+ "test:integration:watch:run": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:run\\\"\"'",
+ "test:integration:watch:open": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:open\\\"\"'",
"test:integration:run": "cypress run --config-file ./cypress/cypress.config.ts --quiet",
"test:integration:open": "cypress open --config-file ./cypress/cypress.config.ts",
"mock": "pnpm mock:build && pnpm mock:run",
@@ -23,6 +25,7 @@
"build": "next build",
"prestart": "pnpm build",
"start": "next start",
+ "start:built": "next start",
"clean": "pnpm mock:destroy && rm -rf .turbo && rm -rf node_modules && rm -rf .next"
},
"git": {
@@ -32,57 +35,58 @@
"*": "prettier --write --ignore-unknown"
},
"dependencies": {
- "@headlessui/react": "^1.7.18",
+ "@headlessui/react": "^2.1.9",
"@heroicons/react": "2.1.3",
"@tailwindcss/forms": "0.5.7",
"@vercel/analytics": "^1.2.2",
"@zitadel/client": "workspace:*",
- "@zitadel/node": "workspace:*",
"@zitadel/proto": "workspace:*",
- "@zitadel/react": "workspace:*",
"clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3",
+ "deepmerge": "^4.3.1",
"moment": "^2.29.4",
- "next": "14.2.3",
+ "next": "15.0.4-canary.23",
+ "next-intl": "^3.25.1",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",
"qrcode.react": "^3.1.0",
- "react": "18.3.1",
- "react-dom": "18.3.1",
+ "react": "19.0.0-rc-66855b96-20241106",
+ "react-dom": "19.0.0-rc-66855b96-20241106",
"react-hook-form": "7.39.5",
"swr": "^2.2.0",
"tinycolor2": "1.4.2"
},
"devDependencies": {
- "@bufbuild/buf": "^1.35.1",
- "@testing-library/jest-dom": "^6.4.5",
- "@testing-library/react": "^15.0.7",
- "@types/ms": "0.7.31",
- "@types/node": "18.11.9",
- "@types/react": "18.2.8",
- "@types/react-dom": "18.0.9",
+ "@bufbuild/buf": "^1.46.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.0.1",
+ "@types/ms": "0.7.34",
+ "@types/node": "22.9.0",
+ "@types/react": "npm:types-react@19.0.0-rc.1",
+ "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/tinycolor2": "1.4.3",
- "@types/uuid": "^9.0.1",
+ "@types/uuid": "^10.0.0",
"@vercel/git-hooks": "1.0.0",
"@zitadel/prettier-config": "workspace:*",
"@zitadel/tsconfig": "workspace:*",
- "autoprefixer": "10.4.13",
- "concurrently": "^8.1.0",
- "cypress": "^13.9.0",
- "del-cli": "5.0.0",
+ "autoprefixer": "10.4.20",
+ "concurrently": "^9.1.0",
+ "cypress": "^13.15.2",
+ "del-cli": "6.0.0",
"env-cmd": "^10.1.0",
"eslint-config-zitadel": "workspace:*",
- "grpc-tools": "1.11.3",
- "lint-staged": "13.0.3",
- "make-dir-cli": "3.0.0",
- "nodemon": "^2.0.22",
- "postcss": "8.4.21",
- "prettier-plugin-tailwindcss": "0.1.13",
- "sass": "^1.77.1",
- "start-server-and-test": "^2.0.0",
- "tailwindcss": "3.2.4",
- "ts-proto": "^1.139.0",
- "typescript": "^5.4.5",
+ "grpc-tools": "1.12.4",
+ "jsdom": "^25.0.1",
+ "lint-staged": "15.2.10",
+ "make-dir-cli": "4.0.0",
+ "nodemon": "^3.1.7",
+ "postcss": "8.4.49",
+ "prettier-plugin-tailwindcss": "0.6.8",
+ "sass": "^1.80.7",
+ "start-server-and-test": "^2.0.8",
+ "tailwindcss": "3.4.14",
+ "ts-proto": "^2.2.7",
+ "typescript": "^5.6.3",
"zitadel-tailwind-config": "workspace:*"
}
}
diff --git a/apps/login/postcss.config.js b/apps/login/postcss.config.cjs
similarity index 100%
rename from apps/login/postcss.config.js
rename to apps/login/postcss.config.cjs
diff --git a/apps/login/public/checkbox.svg b/apps/login/public/checkbox.svg
new file mode 100644
index 0000000000..94a3298ae6
--- /dev/null
+++ b/apps/login/public/checkbox.svg
@@ -0,0 +1 @@
+
diff --git a/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg
index df44ec5398..4a4e8be71b 100644
--- a/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg
+++ b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg
@@ -1,6 +1,6 @@
-