diff --git a/.devcontainer/base/Dockerfile b/.devcontainer/base/Dockerfile
new file mode 100644
index 0000000000..119594bc6f
--- /dev/null
+++ b/.devcontainer/base/Dockerfile
@@ -0,0 +1,16 @@
+FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm
+
+ENV SHELL=/bin/bash \
+ DEBIAN_FRONTEND=noninteractive \
+ LANG=C.UTF-8 \
+ LC_ALL=C.UTF-8 \
+ CI=1 \
+ PNPM_HOME=/home/node/.local/share/pnpm \
+ PATH=/home/node/.local/share/pnpm:$PATH
+
+
+RUN apt-get update && \
+ apt-get --no-install-recommends install -y \
+ libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb && \
+ apt-get clean && \
+ corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@9.1.2 --activate
diff --git a/.devcontainer/base/devcontainer.json b/.devcontainer/base/devcontainer.json
new file mode 100644
index 0000000000..187e012e67
--- /dev/null
+++ b/.devcontainer/base/devcontainer.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json",
+ "name": "devcontainer",
+ "dockerComposeFile": "docker-compose.yml",
+ "service": "devcontainer",
+ "workspaceFolder": "/workspaces",
+ "features": {
+ "ghcr.io/devcontainers/features/go:1": {
+ "version": "1.24"
+ },
+ "ghcr.io/guiyomh/features/golangci-lint:0": {},
+ "ghcr.io/jungaretti/features/make:1": {}
+ },
+ "forwardPorts": [
+ 3000,
+ 3001,
+ 4200,
+ 8080
+ ],
+ "onCreateCommand": "pnpm install -g sass@1.64.1",
+ "customizations": {
+ "jetbrains": {
+ "settings": {
+ "com.intellij:app:HttpConfigurable.use_proxy_pac": true
+ }
+ }
+ }
+}
diff --git a/.devcontainer/base/docker-compose.yml b/.devcontainer/base/docker-compose.yml
new file mode 100644
index 0000000000..d1b26f1a7b
--- /dev/null
+++ b/.devcontainer/base/docker-compose.yml
@@ -0,0 +1,225 @@
+services:
+
+ devcontainer:
+ container_name: devcontainer
+ build:
+ context: .
+ volumes:
+ - ../../:/workspaces:cached
+ - /tmp/.X11-unix:/tmp/.X11-unix:cached
+ - home-dir:/home/node:delegated
+ command: sleep infinity
+ working_dir: /workspaces
+ environment:
+ ZITADEL_DATABASE_POSTGRES_HOST: db
+ ZITADEL_EXTERNALSECURE: false
+
+ db:
+ container_name: db
+ image: postgres:17.0-alpine3.19
+ restart: unless-stopped
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ environment:
+ PGUSER: postgres
+ POSTGRES_PASSWORD: postgres
+ healthcheck:
+ test: [ "CMD-SHELL", "pg_isready" ]
+ interval: "10s"
+ timeout: "30s"
+ retries: 5
+ start_period: "20s"
+ ports:
+ - "5432:5432"
+
+ mock-zitadel:
+ container_name: mock-zitadel
+ build:
+ context: ../../apps/login/integration/core-mock
+ ports:
+ - 22220:22220
+ - 22222:22222
+
+ login-integration:
+ container_name: login-integration
+ build:
+ context: ../..
+ dockerfile: build/login/Dockerfile
+ image: "${LOGIN_TAG:-zitadel-login:local}"
+ env_file: ../../apps/login/.env.test
+ network_mode: service:devcontainer
+ environment:
+ NODE_ENV: test
+ PORT: 3001
+ depends_on:
+ mock-zitadel:
+ condition: service_started
+
+ zitadel:
+ image: "${ZITADEL_TAG:-ghcr.io/zitadel/zitadel:v4.0.0-rc.2}"
+ container_name: zitadel
+ command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml'
+ volumes:
+ - ../../apps/login/acceptance/pat:/pat:delegated
+ - ../../apps/login/acceptance/zitadel.yaml:/zitadel.yaml:cached
+ network_mode: service:devcontainer
+ healthcheck:
+ test:
+ - CMD
+ - /app/zitadel
+ - ready
+ - --config
+ - /zitadel.yaml
+ depends_on:
+ db:
+ condition: "service_healthy"
+
+ configure-login:
+ container_name: configure-login
+ restart: no
+ build:
+ context: ../../apps/login/acceptance/setup
+ dockerfile: ../go-command.Dockerfile
+ entrypoint: "./setup.sh"
+ network_mode: service:devcontainer
+ environment:
+ PAT_FILE: /pat/zitadel-admin-sa.pat
+ ZITADEL_API_URL: http://localhost:8080
+ WRITE_ENVIRONMENT_FILE: /login-env/.env.test.local
+ SINK_EMAIL_INTERNAL_URL: http://sink:3333/email
+ SINK_SMS_INTERNAL_URL: http://sink:3333/sms
+ SINK_NOTIFICATION_URL: http://sink:3333/notification
+ LOGIN_BASE_URL: http://localhost:3000/ui/v2/login/
+ ZITADEL_API_DOMAIN: localhost
+ ZITADEL_ADMIN_USER: zitadel-admin@zitadel.localhost
+ volumes:
+ - ../../apps/login/acceptance/pat:/pat:cached # Read the PAT file from zitadels setup
+ - ../../apps/login:/login-env:delegated # Write the environment variables file for the login
+ depends_on:
+ zitadel:
+ condition: "service_healthy"
+
+ login-acceptance:
+ container_name: login
+ image: "${LOGIN_TAG:-ghcr.io/zitadel/zitadel-login:v4.0.0-rc.2}"
+ network_mode: service:devcontainer
+ volumes:
+ - ../../apps/login/.env.test.local:/env-files/.env:cached
+ depends_on:
+ configure-login:
+ condition: service_completed_successfully
+
+ mock-notifications:
+ container_name: mock-notifications
+ build:
+ context: ../../apps/login/acceptance/sink
+ dockerfile: ../go-command.Dockerfile
+ args:
+ - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
+ environment:
+ PORT: '3333'
+ command:
+ - -port
+ - '3333'
+ - -email
+ - '/email'
+ - -sms
+ - '/sms'
+ - -notification
+ - '/notification'
+ ports:
+ - "3333:3333"
+ depends_on:
+ configure-login:
+ condition: "service_completed_successfully"
+
+ mock-oidcrp:
+ container_name: mock-oidcrp
+ build:
+ context: ../../apps/login/acceptance/oidcrp
+ dockerfile: ../go-command.Dockerfile
+ args:
+ - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
+ network_mode: service:devcontainer
+ environment:
+ API_URL: 'http://localhost:8080'
+ API_DOMAIN: 'localhost'
+ PAT_FILE: '/pat/zitadel-admin-sa.pat'
+ LOGIN_URL: 'http://localhost:3000/ui/v2/login'
+ ISSUER: 'http://localhost:8000'
+ HOST: 'localhost'
+ PORT: '8000'
+ SCOPES: 'openid profile email'
+ volumes:
+ - ../../apps/login/acceptance/pat:/pat:cached
+ depends_on:
+ configure-login:
+ condition: "service_completed_successfully"
+
+ # mock-oidcop:
+ # container_name: mock-oidcop
+ # build:
+ # context: ../../apps/login/acceptance/idp/oidc
+ # dockerfile: ../../go-command.Dockerfile
+ # args:
+ # - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
+ # network_mode: service:devcontainer
+ # environment:
+ # API_URL: 'http://localhost:8080'
+ # API_DOMAIN: 'localhost'
+ # PAT_FILE: '/pat/zitadel-admin-sa.pat'
+ # SCHEMA: 'http'
+ # HOST: 'localhost'
+ # PORT: "8004"
+ # volumes:
+ # - "../apps/login/packages/acceptance/pat:/pat:cached"
+ # depends_on:
+ # configure-login:
+ # condition: "service_completed_successfully"
+
+ mock-samlsp:
+ container_name: mock-samlsp
+ build:
+ context: ../../apps/login/acceptance/samlsp
+ dockerfile: ../go-command.Dockerfile
+ args:
+ - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
+ network_mode: service:devcontainer
+ environment:
+ API_URL: 'http://localhost:8080'
+ API_DOMAIN: 'localhost'
+ PAT_FILE: '/pat/zitadel-admin-sa.pat'
+ LOGIN_URL: 'http://localhost:3000/ui/v2/login'
+ IDP_URL: 'http://localhost:8080/saml/v2/metadata'
+ HOST: 'http://localhost:8001'
+ PORT: '8001'
+ volumes:
+ - "../apps/login/packages/acceptance/pat:/pat:cached"
+ depends_on:
+ configure-login:
+ condition: "service_completed_successfully"
+
+# mock-samlidp:
+# container_name: mock-samlidp
+# build:
+# context: ../../apps/login/acceptance/idp/saml
+# dockerfile: ../../go-command.Dockerfile
+# args:
+# - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
+# network_mode: service:devcontainer
+# environment:
+# API_URL: 'http://localhost:8080'
+# API_DOMAIN: 'localhost'
+# PAT_FILE: '/pat/zitadel-admin-sa.pat'
+# SCHEMA: 'http'
+# HOST: 'localhost'
+# PORT: "8003"
+# volumes:
+# - "../apps/login/packages/acceptance/pat:/pat"
+# depends_on:
+# configure-login:
+# condition: "service_completed_successfully"
+
+volumes:
+ postgres-data:
+ home-dir:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
deleted file mode 100644
index 5d49f92cf4..0000000000
--- a/.devcontainer/devcontainer.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "zitadel",
- "dockerComposeFile": "docker-compose.yml",
- "service": "devcontainer",
- "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
- "features": {
- "ghcr.io/devcontainers/features/go:1": {
- "version": "1.22"
- },
- "ghcr.io/devcontainers/features/node:1": {},
- "ghcr.io/guiyomh/features/golangci-lint:0": {},
- "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
- "ghcr.io/devcontainers/features/github-cli:1": {},
- "ghcr.io/jungaretti/features/make:1": {}
- },
- "forwardPorts": [
- 3000,
- 4200,
- 8080
- ],
- "onCreateCommand": "npm install -g sass@1.64.1"
-}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
deleted file mode 100644
index cece28632b..0000000000
--- a/.devcontainer/docker-compose.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-version: '3.8'
-services:
- devcontainer:
- image: mcr.microsoft.com/devcontainers/base:ubuntu
- volumes:
- - ../..:/workspaces:cached
- - /var/run/docker.sock:/var/run/docker.sock
- network_mode: service:db
- command: sleep infinity
- environment:
- ZITADEL_DATABASE_POSTGRES_HOST: db
- ZITADEL_DATABASE_POSTGRES_PORT: 5432
- ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
- ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
- ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel
- ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
- ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
- ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
- ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
- ZITADEL_EXTERNALSECURE: false
- db:
- image: postgres:latest
- restart: unless-stopped
- volumes:
- - postgres-data:/var/lib/postgresql/data
- environment:
- PGUSER: postgres
- POSTGRES_PASSWORD: postgres
-
-volumes:
- postgres-data:
diff --git a/.devcontainer/login-integration-debug/devcontainer.json b/.devcontainer/login-integration-debug/devcontainer.json
new file mode 100644
index 0000000000..525b04788e
--- /dev/null
+++ b/.devcontainer/login-integration-debug/devcontainer.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json",
+ "name": "login-integration-debug",
+ "dockerComposeFile": [
+ "../base/docker-compose.yml",
+ "docker-compose.yml"
+ ],
+ "service": "login-integration-debug",
+ "runServices": ["login-integration-debug"],
+ "workspaceFolder": "/workspaces",
+ "forwardPorts": [3001],
+ "onCreateCommand": "pnpm install --recursive",
+ "postAttachCommand": "pnpm turbo daemon clean; pnpm turbo @zitadel/login#dev test:integration:login:debug",
+ "customizations": {
+ "jetbrains": {
+ "settings": {
+ "com.intellij:app:HttpConfigurable.use_proxy_pac": true
+ }
+ }
+ }
+}
diff --git a/.devcontainer/login-integration-debug/docker-compose.yml b/.devcontainer/login-integration-debug/docker-compose.yml
new file mode 100644
index 0000000000..11ce02ee7d
--- /dev/null
+++ b/.devcontainer/login-integration-debug/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ login-integration-debug:
+ extends:
+ file: ../base/docker-compose.yml
+ service: devcontainer
+ container_name: login-integration-debug
+ depends_on:
+ mock-zitadel:
+ condition: service_started
diff --git a/.devcontainer/login-integration/devcontainer.json b/.devcontainer/login-integration/devcontainer.json
new file mode 100644
index 0000000000..1b7e02df43
--- /dev/null
+++ b/.devcontainer/login-integration/devcontainer.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json",
+ "name": "login-integration",
+ "dockerComposeFile": [
+ "../base/docker-compose.yml"
+ ],
+ "service": "devcontainer",
+ "runServices": ["login-integration"],
+ "workspaceFolder": "/workspaces",
+ "forwardPorts": [3001],
+ "onCreateCommand": "pnpm install --frozen-lockfile --recursive && cd apps/login/packages/integration && pnpm cypress install && pnpm test:integration:login",
+ "customizations": {
+ "jetbrains": {
+ "settings": {
+ "com.intellij:app:HttpConfigurable.use_proxy_pac": true
+ }
+ }
+ }
+}
diff --git a/.devcontainer/turbo-lint-unit-debug/devcontainer.json b/.devcontainer/turbo-lint-unit-debug/devcontainer.json
new file mode 100644
index 0000000000..19446687a1
--- /dev/null
+++ b/.devcontainer/turbo-lint-unit-debug/devcontainer.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json",
+ "name": "turbo-lint-unit-debug",
+ "dockerComposeFile": [
+ "../base/docker-compose.yml",
+ "docker-compose.yml"
+ ],
+ "service": "turbo-lint-unit-debug",
+ "runServices": ["turbo-lint-unit-debug"],
+ "workspaceFolder": "/workspaces",
+ "forwardPorts": [3001],
+ "onCreateCommand": "pnpm install --recursive",
+ "postAttachCommand": "pnpm turbo daemon clean; pnpm turbo watch lint test:unit",
+ "customizations": {
+ "jetbrains": {
+ "settings": {
+ "com.intellij:app:HttpConfigurable.use_proxy_pac": true
+ }
+ }
+ }
+}
diff --git a/.devcontainer/turbo-lint-unit-debug/docker-compose.yml b/.devcontainer/turbo-lint-unit-debug/docker-compose.yml
new file mode 100644
index 0000000000..a19a0211e5
--- /dev/null
+++ b/.devcontainer/turbo-lint-unit-debug/docker-compose.yml
@@ -0,0 +1,6 @@
+services:
+ turbo-lint-unit-debug:
+ extends:
+ file: ../base/docker-compose.yml
+ service: devcontainer
+ container_name: turbo-lint-unit-debug
diff --git a/.devcontainer/turbo-lint-unit/devcontainer.json b/.devcontainer/turbo-lint-unit/devcontainer.json
new file mode 100644
index 0000000000..f3c4f64355
--- /dev/null
+++ b/.devcontainer/turbo-lint-unit/devcontainer.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json",
+ "name": "turbo-lint-unit",
+ "dockerComposeFile": [
+ "../base/docker-compose.yml"
+ ],
+ "service": "devcontainer",
+ "runServices": ["devcontainer"],
+ "workspaceFolder": "/workspaces",
+ "postStartCommand": "pnpm install --frozen-lockfile --recursive && pnpm turbo lint test:unit",
+ "customizations": {
+ "jetbrains": {
+ "settings": {
+ "com.intellij:app:HttpConfigurable.use_proxy_pac": true
+ }
+ }
+ }
+}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b7354f3f4a..8fa71ba652 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -22,6 +22,19 @@ updates:
commit-message:
prefix: chore
include: scope
+- package-ecosystem: npm
+ directory: '/login'
+ open-pull-requests-limit: 3
+ schedule:
+ interval: daily
+ groups:
+ prod:
+ dependency-type: production
+ dev:
+ dependency-type: development
+ ignore:
+ - dependency-name: "eslint"
+ versions: [ "9.x" ]
- package-ecosystem: gomod
groups:
go:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f06c4a959c..e501eb169b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,6 +18,7 @@ permissions:
packages: write
issues: write
pull-requests: write
+ actions: write
jobs:
core:
@@ -30,6 +31,11 @@ jobs:
uses: ./.github/workflows/console.yml
with:
node_version: "20"
+
+ docs:
+ uses: ./.github/workflows/docs.yml
+ with:
+ node_version: "20"
buf_version: "latest"
version:
@@ -47,6 +53,9 @@ jobs:
core_cache_path: ${{ needs.core.outputs.cache_path }}
console_cache_path: ${{ needs.console.outputs.cache_path }}
version: ${{ needs.version.outputs.version }}
+ node_version: "20"
+ secrets:
+ DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
core-unit-test:
needs: core
@@ -86,6 +95,17 @@ jobs:
with:
build_image_name: "ghcr.io/zitadel/zitadel-build"
+ login-container:
+ uses: ./.github/workflows/login-container.yml
+ permissions:
+ packages: write
+ id-token: write
+ with:
+ login_build_image_name: "ghcr.io/zitadel/zitadel-login-build"
+ node_version: "20"
+ secrets:
+ DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
+
e2e:
uses: ./.github/workflows/e2e.yml
needs: [compile]
@@ -98,7 +118,15 @@ jobs:
issues: write
pull-requests: write
needs:
- [version, core-unit-test, core-integration-test, lint, container, e2e]
+ [
+ version,
+ core-unit-test,
+ core-integration-test,
+ lint,
+ container,
+ login-container,
+ e2e,
+ ]
if: ${{ github.event_name == 'workflow_dispatch' }}
secrets:
GCR_JSON_KEY_BASE64: ${{ secrets.GCR_JSON_KEY_BASE64 }}
@@ -109,3 +137,6 @@ jobs:
semantic_version: "23.0.7"
image_name: "ghcr.io/zitadel/zitadel"
google_image_name: "europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel"
+ build_image_name_login: ${{ needs.login-container.outputs.login_build_image }}
+ image_name_login: "ghcr.io/zitadel/zitadel-login"
+ google_image_name_login: "europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel-login"
diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml
index 519586b9ee..65e7851d48 100644
--- a/.github/workflows/compile.yml
+++ b/.github/workflows/compile.yml
@@ -18,6 +18,12 @@ on:
version:
required: true
type: string
+ node_version:
+ required: true
+ type: string
+ secrets:
+ DEPOT_TOKEN:
+ required: true
jobs:
executable:
@@ -27,70 +33,59 @@ jobs:
matrix:
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
-
+
steps:
- -
- uses: actions/checkout@v4
- -
- uses: actions/cache/restore@v4
- timeout-minutes: 1
- name: restore console
- with:
- path: ${{ inputs.console_cache_path }}
- key: ${{ inputs.console_cache_key }}
- fail-on-cache-miss: true
- -
- uses: actions/cache/restore@v4
- timeout-minutes: 1
- name: restore core
- with:
- path: ${{ inputs.core_cache_path }}
- key: ${{ inputs.core_cache_key }}
- fail-on-cache-miss: true
- -
- uses: actions/setup-go@v5
- with:
- go-version-file: 'go.mod'
- -
- name: compile
- timeout-minutes: 5
- run: |
- GOOS="${{matrix.goos}}" \
- GOARCH="${{matrix.goarch}}" \
- VERSION="${{ inputs.version }}" \
- COMMIT_SHA="${{ github.sha }}" \
- make compile_pipeline
- -
- name: create folder
- run: |
- mkdir zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
- mv zitadel zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
- cp LICENSE zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
- cp README.md zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
- tar -czvf zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
- -
- uses: actions/upload-artifact@v4
- with:
- name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
- path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
-
+ - uses: actions/checkout@v4
+ - uses: actions/cache/restore@v4
+ timeout-minutes: 1
+ name: restore console
+ with:
+ path: ${{ inputs.console_cache_path }}
+ key: ${{ inputs.console_cache_key }}
+ fail-on-cache-miss: true
+ - uses: actions/cache/restore@v4
+ timeout-minutes: 1
+ name: restore core
+ with:
+ path: ${{ inputs.core_cache_path }}
+ key: ${{ inputs.core_cache_key }}
+ fail-on-cache-miss: true
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: "go.mod"
+ - name: compile
+ timeout-minutes: 5
+ run: |
+ GOOS="${{matrix.goos}}" \
+ GOARCH="${{matrix.goarch}}" \
+ VERSION="${{ inputs.version }}" \
+ COMMIT_SHA="${{ github.sha }}" \
+ make compile_pipeline
+ - name: create folder
+ run: |
+ mkdir zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
+ mv zitadel zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
+ cp LICENSE zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
+ cp README.md zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/
+ tar -czvf zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
+ - uses: actions/upload-artifact@v4
+ with:
+ name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
+ path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
+
checksums:
runs-on: ubuntu-latest
- needs: executable
+ needs: [executable]
steps:
- -
- uses: actions/download-artifact@v4
- with:
- path: executables
- -
- name: move files one folder up
- run: mv */*.tar.gz . && find . -type d -empty -delete
- working-directory: executables
- -
- run: sha256sum * > checksums.txt
- working-directory: executables
- -
- uses: actions/upload-artifact@v4
- with:
- name: checksums.txt
- path: executables/checksums.txt
+ - uses: actions/download-artifact@v4
+ with:
+ path: executables
+ - name: move files one folder up
+ run: mv */*.tar.gz . && find . -type d -empty -delete
+ working-directory: executables
+ - run: sha256sum * > checksums.txt
+ working-directory: executables
+ - uses: actions/upload-artifact@v4
+ with:
+ name: checksums.txt
+ path: executables/checksums.txt
diff --git a/.github/workflows/console.yml b/.github/workflows/console.yml
index 3e77757129..b2f8119190 100644
--- a/.github/workflows/console.yml
+++ b/.github/workflows/console.yml
@@ -1,19 +1,16 @@
name: Build console
-on:
+on:
workflow_call:
inputs:
node_version:
required: true
type: string
- buf_version:
- required: true
- type: string
outputs:
cache_key:
value: ${{ jobs.build.outputs.cache_key }}
cache_path:
- value: ${{ jobs.build.outputs.cache_path }}
+ value: ${{ jobs.build.outputs.cache_path }}
env:
cache_path: console/dist/console
@@ -25,38 +22,32 @@ jobs:
cache_path: ${{ env.cache_path }}
runs-on: ubuntu-latest
steps:
- -
- uses: actions/checkout@v4
- -
- uses: actions/cache/restore@v4
- timeout-minutes: 1
- continue-on-error: true
- id: cache
- with:
- key: console-${{ hashFiles('console', 'proto', '!console/dist') }}
- restore-keys: |
- console-
- path: ${{ env.cache_path }}
- -
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
- uses: bufbuild/buf-setup-action@v1
- with:
- github_token: ${{ github.token }}
- version: ${{ inputs.buf_version }}
- -
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ inputs.node_version }}
- cache: 'yarn'
- cache-dependency-path: console/yarn.lock
- -
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
- run: make console_build
- -
- if: ${{ steps.cache.outputs.cache-hit != 'true' }}
- uses: actions/cache/save@v4
- with:
- path: ${{ env.cache_path }}
- key: ${{ steps.cache.outputs.cache-primary-key }}
-
\ No newline at end of file
+ - uses: actions/checkout@v4
+ - uses: actions/cache/restore@v4
+ timeout-minutes: 1
+ continue-on-error: true
+ id: cache
+ with:
+ key: console-${{ hashFiles('console', 'proto', '!console/dist') }}
+ restore-keys: |
+ console-
+ path: ${{ env.cache_path }}
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: pnpm/action-setup@v4
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ inputs.node_version }}
+ cache: "pnpm"
+ cache-dependency-path: pnpm-lock.yaml
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ name: Install dependencies
+ run: pnpm install --frozen-lockfile
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ name: Build console with Turbo
+ run: pnpm turbo build --filter=./console
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: actions/cache/save@v4
+ with:
+ path: ${{ env.cache_path }}
+ key: ${{ steps.cache.outputs.cache-primary-key }}
diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml
index 33ffd4f6af..f762124e00 100644
--- a/.github/workflows/container.yml
+++ b/.github/workflows/container.yml
@@ -79,7 +79,7 @@ jobs:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
- file: build/Dockerfile
+ file: build/zitadel/Dockerfile
target: artifact
platforms: linux/${{ matrix.arch }}
push: true
@@ -94,7 +94,7 @@ jobs:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
- file: build/Dockerfile
+ file: build/zitadel/Dockerfile
target: final
platforms: linux/${{ matrix.arch }}
push: true
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index 13e7c0dee7..c864c650a7 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -25,6 +25,7 @@ env:
internal/api/assets/router.go
openapi/v2
pkg/grpc/**/*.pb.*
+ pkg/grpc/**/*.connect.go
jobs:
build:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000000..2f0ac3d7af
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,61 @@
+name: Build docs
+
+on:
+ workflow_call:
+ inputs:
+ node_version:
+ required: true
+ type: string
+ buf_version:
+ required: true
+ type: string
+ outputs:
+ cache_key:
+ value: ${{ jobs.build.outputs.cache_key }}
+ cache_path:
+ value: ${{ jobs.build.outputs.cache_path }}
+
+env:
+ cache_path: docs/build
+
+jobs:
+ build:
+ outputs:
+ cache_key: ${{ steps.cache.outputs.cache-primary-key }}
+ cache_path: ${{ env.cache_path }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/cache/restore@v4
+ timeout-minutes: 1
+ continue-on-error: true
+ id: cache
+ with:
+ key: docs-${{ hashFiles('docs', 'proto', '!docs/build', '!docs/node_modules', '!docs/protoc-gen-connect-openapi') }}
+ restore-keys: |
+ docs-
+ path: ${{ env.cache_path }}
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: bufbuild/buf-setup-action@v1
+ with:
+ github_token: ${{ github.token }}
+ version: ${{ inputs.buf_version }}
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: pnpm/action-setup@v4
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ inputs.node_version }}
+ cache: "pnpm"
+ cache-dependency-path: pnpm-lock.yaml
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ name: Install dependencies
+ run: pnpm install
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ name: Build docs with Turbo
+ run: pnpm turbo build --filter=./docs
+ - if: ${{ steps.cache.outputs.cache-hit != 'true' }}
+ uses: actions/cache/save@v4
+ with:
+ path: ${{ env.cache_path }}
+ key: ${{ steps.cache.outputs.cache-primary-key }}
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index e717163507..b9c2159e1c 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -12,44 +12,47 @@ jobs:
browser: [firefox, chrome]
runs-on: ubuntu-latest
steps:
- -
- name: Checkout Repository
+ - name: Checkout Repository
uses: actions/checkout@v4
- -
- uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v4
with:
path: .artifacts
name: zitadel-linux-amd64
- -
- name: Unpack executable
+ - name: Unpack executable
run: |
tar -xvf .artifacts/zitadel-linux-amd64.tar.gz
mv zitadel-linux-amd64/zitadel ./zitadel
- -
- name: Set up QEMU
+ - name: Set up QEMU
uses: docker/setup-qemu-action@v3
- -
- name: Set up Docker Buildx
+ - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- -
- name: Start DB and ZITADEL
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: "pnpm"
+ cache-dependency-path: pnpm-lock.yaml
+ - name: Install dependencies
+ run: pnpm install
+ - name: Install Cypress binary
+ run: cd ./e2e && pnpm exec cypress install
+ - name: Start DB and ZITADEL
run: |
cd ./e2e
ZITADEL_IMAGE=zitadel:local docker compose up --detach --wait
- -
- name: Cypress run
+ - name: Cypress run
uses: cypress-io/github-action@v6
env:
CYPRESS_BASE_URL: http://localhost:8080/ui/console
CYPRESS_WEBHOOK_HANDLER_HOST: host.docker.internal
- CYPRESS_DATABASE_CONNECTION_URL: 'postgresql://root@localhost:26257/zitadel'
+ CYPRESS_DATABASE_CONNECTION_URL: "postgresql://root@localhost:26257/zitadel"
CYPRESS_BACKEND_URL: http://localhost:8080
with:
working-directory: e2e
browser: ${{ matrix.browser }}
config-file: cypress.config.ts
- -
- uses: actions/upload-artifact@v4
+ install: false
+ - uses: actions/upload-artifact@v4
if: always()
with:
name: production-tests-${{ matrix.browser }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index e704bdb146..b8c7486f1f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,7 +20,6 @@ on:
type: string
jobs:
-
lint-skip:
name: lint skip
runs-on: ubuntu-latest
@@ -36,64 +35,50 @@ jobs:
continue-on-error: true
if: ${{ github.event_name == 'pull_request' }}
steps:
- -
- uses: actions/checkout@v4
- -
- uses: bufbuild/buf-setup-action@v1
- with:
- version: ${{ inputs.buf_version }}
- github_token: ${{ secrets.GITHUB_TOKEN }}
- -
- name: lint
- uses: bufbuild/buf-lint-action@v1
- -
- uses: bufbuild/buf-breaking-action@v1
- with:
- against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
+ - uses: actions/checkout@v4
+ - uses: bufbuild/buf-setup-action@v1
+ with:
+ version: ${{ inputs.buf_version }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ - name: lint
+ uses: bufbuild/buf-lint-action@v1
+ - uses: bufbuild/buf-breaking-action@v1
+ with:
+ against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
- console:
+ turbo-lint-unit:
if: ${{ github.event_name == 'pull_request' }}
- name: console
+ name: turbo-lint-unit
runs-on: ubuntu-latest
steps:
- -
- name: Checkout
- uses: actions/checkout@v4
- -
- uses: actions/setup-node@v4
- with:
- node-version: ${{ inputs.node_version }}
- cache: 'yarn'
- cache-dependency-path: console/yarn.lock
- -
- run: cd console && yarn install
- -
- name: lint
- run: make console_lint
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Run lint and unit tests in dev container
+ uses: devcontainers/ci@v0.3
+ with:
+ push: never
+ configFile: .devcontainer/turbo-lint-unit/devcontainer.json
+ runCmd: echo "Successfully ran lint and unit tests in dev container postStartCommand"
core:
name: core
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
- -
- name: Checkout
- uses: actions/checkout@v4
- -
- uses: actions/setup-go@v5
- with:
- go-version-file: 'go.mod'
- -
- uses: actions/cache/restore@v4
- timeout-minutes: 1
- name: restore core
- with:
- path: ${{ inputs.core_cache_path }}
- key: ${{ inputs.core_cache_key }}
- fail-on-cache-miss: true
- -
- uses: golangci/golangci-lint-action@v6
- with:
- version: ${{ inputs.go_lint_version }}
- github-token: ${{ github.token }}
- only-new-issues: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: "go.mod"
+ - uses: actions/cache/restore@v4
+ timeout-minutes: 1
+ name: restore core
+ with:
+ path: ${{ inputs.core_cache_path }}
+ key: ${{ inputs.core_cache_key }}
+ fail-on-cache-miss: true
+ - uses: golangci/golangci-lint-action@v6
+ with:
+ version: ${{ inputs.go_lint_version }}
+ github-token: ${{ github.token }}
+ only-new-issues: true
diff --git a/.github/workflows/login-container.yml b/.github/workflows/login-container.yml
new file mode 100644
index 0000000000..5137213cc4
--- /dev/null
+++ b/.github/workflows/login-container.yml
@@ -0,0 +1,70 @@
+name: Login Container
+
+on:
+ workflow_call:
+ inputs:
+ login_build_image_name:
+ description: 'The image repository name of the standalone login image'
+ type: string
+ required: true
+ node_version:
+ required: true
+ type: string
+ outputs:
+ login_build_image:
+ description: 'The full image tag of the standalone login image'
+ value: '${{ inputs.login_build_image_name }}:${{ github.sha }}'
+ secrets:
+ DEPOT_TOKEN:
+ required: true
+
+permissions:
+ packages: write
+
+env:
+ default_labels: |
+ org.opencontainers.image.documentation=https://zitadel.com/docs
+ org.opencontainers.image.vendor=CAOS AG
+ org.opencontainers.image.licenses=MIT
+
+jobs:
+ login-container:
+ name: Build Login Container
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: depot/setup-action@v1
+ - name: Login meta
+ id: login-meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ inputs.login_build_image_name }}
+ labels: ${{ env.default_labels}}
+ annotations: |
+ manifest:org.opencontainers.image.licenses=MIT
+ tags: |
+ type=sha,prefix=,suffix=,format=long
+ - name: Login to Docker registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Bake login multi-arch
+ uses: depot/bake-action@v1
+ env:
+ DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
+ NODE_VERSION: ${{ inputs.node_version }}
+ with:
+ push: true
+ provenance: true
+ sbom: true
+ targets: login-standalone
+ project: w47wkxzdtw
+ files: |
+ ./apps/login/docker-bake.hcl
+ ./apps/login/docker-bake-release.hcl
+ ./docker-bake.hcl
+ cwd://${{ steps.login-meta.outputs.bake-file }}
diff --git a/.github/workflows/ready_for_review.yml b/.github/workflows/ready_for_review.yml
index 2ead263dc9..db756633f4 100644
--- a/.github/workflows/ready_for_review.yml
+++ b/.github/workflows/ready_for_review.yml
@@ -13,7 +13,7 @@ jobs:
Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review:
- - [ ] I am happy with the code
+ - [ ] I have reviewed my changes and would approve it
- [ ] Documentations and examples are up-to-date
- [ ] Logical behavior changes are tested automatically
- [ ] No debug or dead code
@@ -28,4 +28,4 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: content
- })
\ No newline at end of file
+ })
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3e40ae8805..bfbc3d6934 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,6 +15,15 @@ on:
google_image_name:
required: true
type: string
+ build_image_name_login:
+ required: true
+ type: string
+ image_name_login:
+ required: true
+ type: string
+ google_image_name_login:
+ required: true
+ type: string
secrets:
GCR_JSON_KEY_BASE64:
description: 'base64 endcrypted key to connect to Google'
@@ -96,6 +105,12 @@ jobs:
docker buildx imagetools create \
--tag ${{ inputs.google_image_name }}:${{ needs.version.outputs.version }} \
${{ inputs.build_image_name }}
+ docker buildx imagetools create \
+ --tag ${{ inputs.image_name_login }}:${{ needs.version.outputs.version }} \
+ ${{ inputs.build_image_name_login }}
+ docker buildx imagetools create \
+ --tag ${{ inputs.google_image_name_login }}:${{ needs.version.outputs.version }} \
+ ${{ inputs.build_image_name_login }}
-
name: Publish latest
if: ${{ github.ref_name == 'next' }}
@@ -106,6 +121,9 @@ jobs:
docker buildx imagetools create \
--tag ${{ inputs.image_name }}:latest-debug \
${{ inputs.build_image_name }}-debug
+ docker buildx imagetools create \
+ --tag ${{ inputs.image_name_login }}:latest \
+ ${{ inputs.build_image_name_login }}
homebrew-tap:
runs-on: ubuntu-22.04
@@ -146,3 +164,56 @@ jobs:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
gh workflow -R zitadel/zitadel-charts run bump.yml
+
+ npm-packages:
+ runs-on: ubuntu-latest
+ needs: version
+ if: ${{ github.ref_name == 'next' }}
+ continue-on-error: true
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Install dependencies
+ working-directory: login
+ run: pnpm install --frozen-lockfile
+
+ - name: Create Release Pull Request
+ uses: changesets/action@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ version: ${{ needs.version.outputs.version }}
+ cwd: packages
+ createGithubReleases: false
+
+ login-repo:
+ runs-on: ubuntu-latest
+ needs: version
+ if: ${{ github.ref_name == 'next' }}
+ continue-on-error: true
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Push Subtree
+ run: make login_push LOGIN_REMOTE_BRANCH=mirror-zitadel-repo
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ commit-message: 'chore: mirror zitadel repo'
+ branch: mirror-zitadel-repo
+ title: 'chore: mirror zitadel repo'
+ body: 'This PR updates the login repository with the latest changes from the zitadel repository.'
+ base: main
+ reviewers: |
+ @peintnermax
+ @eliobischof
diff --git a/.gitignore b/.gitignore
index 23469d4209..4c3c877a18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
# Test binary, build with `go test -c`
*.test
+!/**/.env.test
# Coverage
coverage.txt
@@ -52,7 +53,8 @@ console/src/app/proto/generated/
!pkg/grpc/protoc/v2/options.pb.go
**.proto.mock.go
**.pb.*.go
-**.gen.go
+pkg/**/**.connect.go
+**.gen.go
openapi/**/*.json
/internal/api/assets/authz.go
/internal/api/assets/router.go
@@ -67,6 +69,7 @@ docs/docs/apis/proto
/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css
/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map
zitadel-*-*
+!apps/**/zitadel-*-*
# local
build/local/*.env
@@ -83,7 +86,14 @@ go.work.sum
.netlify
load-test/node_modules
-load-test/yarn-error.log
+load-test/pnpm-debug.log
load-test/dist
load-test/output/*
.vercel
+
+# Turbo
+.turbo/
+**/.turbo/
+
+# PNPM
+.pnpm-store
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..7bd51097f9
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+auto-install-peers = true
+ignore-scripts = "postman-code-generators"
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000000..0a47c855eb
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+lts/iron
\ No newline at end of file
diff --git a/API_DESIGN.md b/API_DESIGN.md
index 11b7766a49..cdf43a71df 100644
--- a/API_DESIGN.md
+++ b/API_DESIGN.md
@@ -135,6 +135,8 @@ message CreateUserRequest {
```
Only allow providing a context where it is required. The context MUST not be provided if not required.
+If the context is required but deferrable, the context can be defaulted.
+For example, creating an Authorization without an organization id will default the organization id to the projects resource owner.
For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id.
However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ce8b9aff89..4c1ae53072 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,5 @@
-# Contributing to ZITADEL
+# Contributing to Zitadel
+
## Introduction
@@ -12,24 +13,29 @@ If you want to give an answer or be part of discussions please be kind. Treat ot
## What can I contribute?
-For people who are new to ZITADEL: We flag issues which are a good starting point to start contributing. You find them [here](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
+For people who are new to Zitadel: We flag issues which are a good starting point to start contributing.
+You find them [here](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
+We add the label "good first issue" for problems we think are a good starting point to contribute to Zitadel.
-Make ZITADEL more popular and give it a ⭐
+- [Issues for first time contributors](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
+- [All issues](https://github.com/zitadel/zitadel/issues)
-Help shaping the future of ZITADEL:
+Help shaping the future of Zitadel:
- Join our [chat](https://zitadel.com/chat) and discuss with us or others.
- Ask or answer questions in the [issues section](https://github.com/zitadel/zitadel/issues)
- Share your thoughts and ideas in the [discussions section](https://github.com/zitadel/zitadel/discussions)
+Make Zitadel more popular and give it a ⭐
+
+Follow [@zitadel](https://twitter.com/zitadel) on twitter
+
[Contribute](#how-to-contribute)
- [Contribute code](#contribute)
-- If you found a mistake on our [docs page](https://zitadel.com/docs) or something is missing please read [the docs section](#contribute-docs)
+- If you found a mistake on our [docs page](https://zitadel.com/docs) or something is missing please read [the docs section](contribute-docs)
- [Translate](#contribute-internationalization) and improve texts
-Follow [@zitadel](https://twitter.com/zitadel) on twitter
-
## How to contribute
We strongly recommend to [talk to us](https://zitadel.com/contact) before you start contributing to streamline our and your work.
@@ -40,6 +46,21 @@ If you are unfamiliar with git have a look at Github's documentation on [creatin
Please draft the pull request as soon as possible.
Go through the following checklist before you submit the final pull request:
+### Components
+
+The code consists of the following parts:
+
+| name | description | language | where to find | Development Guide |
+| --------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------- |
+| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) | [Contribute to Backend](contribute-backend) |
+| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) | [Contribute to Backend](contribute-backend) |
+| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) | [Contribute to Frontend](contribute-frontend) |
+| login | Modern authentication UI built with Next.js | [Next.js](https://nextjs.org), [React](https://reactjs.org), [TypeScript](https://www.typescriptlang.org) | [./login](./login) | [Contribute to Frontend](contribute-frontend) |
+| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) | [Contribute to Frontend](contribute-frontend) |
+| translations | Internationalization files for default languages | YAML | [./console](./console) and [./internal](./internal) | [Contribute Translations](contribute-translations) |
+
+Please follow the guides to validate and test the code before you contribute.
+
### Submit a pull request (PR)
1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the [zitadel/zitadel](https://github.com/zitadel/zitadel) repository on GitHub
@@ -104,25 +125,6 @@ Please make sure you cover your changes with tests before marking a Pull Request
- [ ] Integration tests ensure that certain commands emit expected events that trigger notifications.
- [ ] Integration tests ensure that certain events trigger expected notifications.
-## Contribute
-
-The code consists of the following parts:
-
-| name | description | language | where to find |
-| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------- |
-| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) |
-| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) |
-| login | Server side rendered frontend the user interacts with during login | [go](https://go.dev), [go templates](https://pkg.go.dev/html/template) | [./internal/api/ui/login](./internal/api/ui/login) |
-| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) |
-| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) |
-
-Please validate and test the code before you contribute.
-
-We add the label "good first issue" for problems we think are a good starting point to contribute to ZITADEL.
-
-- [Issues for first time contributors](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
-- [All issues](https://github.com/zitadel/zitadel/issues)
-
### General Guidelines
#### Gender Neutrality and Inclusive Language
@@ -143,34 +145,62 @@ Choose alternative words depending on the context.
### API
-ZITADEL follows an API first approach. This means all features can not only be accessed via the UI but also via the API.
+Zitadel follows an API first approach. This means all features can not only be accessed via the UI but also via the API.
The API is designed to be used by different clients, such as web applications, mobile applications, and other services.
Therefore, the API is designed to be easy to use, consistent, and reliable.
Please check out the dedicated [API guidelines](./API_DESIGN.md) page when contributing to the API.
-### Developing ZITADEL with Dev Containers
-Follow the instructions provided by your code editor/IDE to initiate the development container. This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers". The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container)
+#### >Developing Zitadel with Dev Containers
-When you are connected to the container run the following commands to start ZITADEL.
+You can use dev containers if you'd like to make sure you have the same development environment like the corresponding GitHub PR checks use.
+The following dev containers are available:
+
+- **.devcontainer/base/devcontainer.json**: Contains everything you need to run whatever you want.
+- **.devcontainer/turbo-lint-unit/devcontainer.json**: Runs a dev container that executes frontent linting and unit tests and then exits. This is useful to reproduce the corresponding GitHub PR check.
+- **.devcontainer/turbo-lint-unit-debug/devcontainer.json**: Runs a dev container that executes frontent linting and unit tests in watch mode. You can fix the errors right away and have immediate feedback.
+- **.devcontainer/login-integration/devcontainer.json**: Runs a dev container that executes login integration tests and then exits. This is useful to reproduce the corresponding GitHub PR check.
+- **.devcontainer/login-integration-debug/devcontainer.json**: Runs a dev container that spins up the login in a hot-reloading dev server and executes login integration tests interactively. You can fix the errors right away and have immediate feedback.
+
+You can also run the GitHub PR checks locally in dev containers without having to connect to a dev container.
+
+
+The following pnpm commands use the [devcontainer CLI](https://github.com/devcontainers/cli/) and exit when the checks are done.
+The minimal system requirements are having Docker and the devcontainers CLI installed.
+If you don't have the node_modules installed already, you need to install the devcontainers CLI manually. Run `npm i -g @devcontainers/cli`. Alternatively, the [official Microsoft VS Code extension for Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) offers a command `Dev Containers: Install devcontainer CLI`
+
+
+```bash
+npm run devcontainer:lint-unit
+npm run devcontainer:integration:login
+```
+
+If you don't have NPM installed, copy and execute the scripts from the package.json directly.
+
+To connect to a dev container to have full IDE support, follow the instructions provided by your code editor/IDE to initiate the dev container.
+This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers".
+The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container)
+
+For example, to build and run the Zitadel binary in a dev container, connect your IDE to the dev container described in .devcontainer/base/devcontainer.json.
+Run the following commands inside the container to start Zitadel.
```bash
make compile && ./zitadel start-from-init --masterkey MasterkeyNeedsToHave32Characters --tlsMode disabled
```
-ZITADEL serves traffic as soon as you can see the following log line:
+Zitadel serves traffic as soon as you can see the following log line:
`INFO[0001] server is listening on [::]:8080`
-### Backend/login
+## Contribute Backend Code
-By executing the commands from this section, you run everything you need to develop the ZITADEL backend locally.
+By executing the commands from this section, you run everything you need to develop the Zitadel backend locally.
Using [Docker Compose](https://docs.docker.com/compose/), you run a [PostgreSQL](https://www.postgresql.org/download/) on your local machine.
-With [make](https://www.gnu.org/software/make/), you build a debuggable ZITADEL binary and run it using [delve](https://github.com/go-delve/delve).
+With [make](https://www.gnu.org/software/make/), you build a debuggable Zitadel binary and run it using [delve](https://github.com/go-delve/delve).
Then, you test your changes via the console your binary is serving at http://localhost:8080 and by verifying the database.
Once you are happy with your changes, you run end-to-end tests and tear everything down.
-ZITADEL uses [golangci-lint](https://golangci-lint.run) for code quality checks. Please use [this configuration](.golangci.yaml) when running `golangci-lint`. We recommend to set golangci-lint as linter in your IDE.
+Zitadel uses [golangci-lint](https://golangci-lint.run) for code quality checks. Please use [this configuration](.golangci.yaml) when running `golangci-lint`. We recommend to set golangci-lint as linter in your IDE.
The commands in this section are tested against the following software versions:
@@ -199,10 +229,10 @@ make compile
> Build the binary: `make compile`
You can now run and debug the binary in .artifacts/zitadel/zitadel using your favourite IDE, for example GoLand.
-You can test if ZITADEL does what you expect by using the UI at http://localhost:8080/ui/console.
+You can test if Zitadel does what you expect by using the UI at http://localhost:8080/ui/console.
Also, you can verify the data by running `psql "host=localhost dbname=zitadel sslmode=disable"` and running SQL queries.
-#### Run Local Unit Tests
+### Run Local Unit Tests
To test the code without dependencies, run the unit tests:
@@ -210,11 +240,11 @@ To test the code without dependencies, run the unit tests:
make core_unit_test
```
-#### Run Local Integration Tests
+### Run Local Integration Tests
-Integration tests are run as gRPC clients against a running ZITADEL server binary.
+Integration tests are run as gRPC clients against a running Zitadel server binary.
The server binary is typically [build with coverage enabled](https://go.dev/doc/build-cover).
-It is also possible to run a ZITADEL sever in a debugger and run the integrations tests like that. In order to run the server, a database is required.
+It is also possible to run a Zitadel sever in a debugger and run the integrations tests like that. In order to run the server, a database is required.
In order to prepare the local system, the following will bring up the database, builds a coverage binary, initializes the database and starts the sever.
@@ -237,7 +267,7 @@ To run all available integration tests:
make core_integration_test_packages
```
-When you change any ZITADEL server code, be sure to rebuild and restart the server before the next test run.
+When you change any Zitadel server code, be sure to rebuild and restart the server before the next test run.
```bash
make core_integration_server_stop core_integration_server_start
@@ -251,69 +281,83 @@ make core_integration_server_stop core_integration_db_down
The test binary has the race detector enabled. `core_core_integration_server_stop` checks for any race logs reported by Go and will print them along a `66` exit code when found. Note that the actual race condition may have happened anywhere during the server lifetime, including start, stop or serving gRPC requests during tests.
-#### Run Local End-to-End Tests
+### Run Local End-to-End Tests
To test the whole system, including the console UI and the login UI, run the E2E tests.
```bash
# Build the production docker image
-export ZITADEL_IMAGE=zitadel:local GOOS=linux
+export Zitadel_IMAGE=zitadel:local GOOS=linux
make docker_image
# If you made changes in the e2e directory, make sure you reformat the files
-make console_lint
+pnpm turbo lint:fix --filter=e2e
# Run the tests
-docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml run --service-ports e2e
+docker compose --file ./e2e/docker-compose.yaml run --service-ports e2e
```
When you are happy with your changes, you can cleanup your environment.
```bash
# Stop and remove the docker containers for zitadel and the database
-docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down
+docker compose --file ./e2e/docker-compose.yaml down
```
-#### Run Local End-to-End Tests Against Your Dev Server Console
+### Run Local End-to-End Tests Against Your Dev Server Console
If you also make [changes to the console](#console), you can run the test suite against your locally built backend code and frontend server.
-But you will have to install the relevant node dependencies.
```bash
-# Install dependencies
-(cd ./e2e && npm install)
+# Install dependencies (from repository root)
+pnpm install
# Run the tests interactively
-(cd ./e2e && npm run open:golangangular)
+pnpm run open:golangangular
# Run the tests non-interactively
-(cd ./e2e && npm run e2e:golangangular)
+pnpm run e2e:golangangular
```
When you are happy with your changes, you can cleanup your environment.
```bash
# Stop and remove the docker containers for zitadel and the database
-docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down
+docker compose --file ./e2e/docker-compose.yaml down
```
-### Console
+## Contribute Frontend Code
-By executing the commands from this section, you run everything you need to develop the console locally.
-Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](https://www.postgresql.org/download/) and the [latest release of ZITADEL](https://github.com/zitadel/zitadel/releases/latest) on your local machine.
-You use the ZITADEL container as backend for your console.
-The console is run in your [Node](https://nodejs.org/en/about/) environment using [a local development server for Angular](https://angular.io/cli/serve#ng-serve), so you have fast feedback about your changes.
+This repository uses **pnpm** as package manager and **Turbo** for build orchestration.
+All frontend packages are managed as a monorepo with shared dependencies and optimized builds:
-We use angular-eslint/Prettier for linting/formatting, so please run `yarn lint:fix` before committing. (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
+- [apps/login](contribute-login) (depends on packages/zitadel-client and packages/zitadel-proto)
+- apps/login/integration
+- apps/login/acceptance
+- [console](contribute-console) (depends on packages/zitadel-client)
+- packages/zitadel-client
+- packages/zitadel-proto
+- [docs](contribute-docs)
-Once you are happy with your changes, you run end-to-end tests and tear everything down.
+### Frontend Development Requirements
+
+The frontend components are run in a [Node](https://nodejs.org/en/about/) environment and are managed using the pnpm package manager and the Turborepo orchestrator.
+
+> [!INFO]
+> Some [dev containers are available](dev-containers) for remote development with docker and pipeline debugging in isolated environments.
+> If you don't want to use one of the dev containers, you can develop the frontend components directly on your local machine.
+> To do so, proceed with installing the necessary dependencies.
+
+We use **pnpm** as package manager and **Turbo** for build orchestration. Use angular-eslint/Prettier for linting/formatting.
+VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues during development.
The commands in this section are tested against the following software versions:
- [Docker version 20.10.17](https://docs.docker.com/engine/install/)
-- [Node version v16.17.0](https://nodejs.org/en/download/)
-- [npm version 8.18.0](https://docs.npmjs.com/try-the-latest-stable-version-of-npm)
-- [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies)
+- [Node version v20.x](https://nodejs.org/en/download/)
+- [pnpm version 9.x](https://pnpm.io/installation)
+
+To run tests with Cypress, ensure you have installed the required [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies)
Note for WSL2 on Windows 10
@@ -325,97 +369,165 @@ The commands in this section are tested against the following software versions:
4. When starting XLaunch, make sure to disable access control
+### Contribute to Login
+
+The Login UI is a Next.js application that provides the user interface for authentication flows.
+It's located in the `apps/login` directory and uses pnpm and Turbo for development.
+
+To start developing the login, make sure your system has the [required system dependencies](frontend-dev-requirements) installed.
+
+#### Development Setup
+
+```bash
+# Start from the root of the repository
+# Start the database and Zitadel backend
+docker compose --file ./apps/login/acceptance/docker-compose.yaml up --detach zitadel
+
+# Install dependencies
+pnpm install
+
+# Option 1: Run login development server with Turbo (recommended)
+pnpm turbo dev --filter=@zitadel/login
+
+# Option 2: Build and serve login (production build)
+pnpm turbo build --filter=@zitadel/login
+cd ./login && pnpm start
+```
+
+The login UI is available at http://localhost:3000.
+
+#### Login Architecture
+
+The login application consists of multiple packages:
+
+- `@zitadel/login` - Main Next.js application
+- `@zitadel/client` - TypeScript client library for Zitadel APIs
+- `@zitadel/proto` - Protocol buffer definitions and generated code
+
+The build process uses Turbo to orchestrate dependencies:
+
+1. Proto generation (`@zitadel/proto#generate`)
+2. Client library build (`@zitadel/client#build`)
+3. Login application build (`@zitadel/login#build`)
+
+#### Pass Quality Checks
+
+Reproduce the pipelines linting and testing for the login.
+
+```bash
+pnpm turbo quality --filter=./apps/login/* --filter=./packages/*
+```
+
+Fix the [quality checks](troubleshoot-frontend), add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass.
+
+### Contribute to Console
+
+To start developing the console, make sure your system has the [required system dependencies](frontend-dev-requirements) installed.
+Then, you need to decide which Zitadel instance you would like to target.
+- The easiest starting point is to [configure your environment](console-dev-existing-zitadel) to use a [Zitadel cloud](https://zitadel.com) instance.
+- Alternatively, you can [start a local Zitadel instance from scratch and develop against it](console-dev-local-zitadel).
+
+#### Develop against an already running Zitadel instance
+
+By default, `pnpm dev --filter=console` targets a Zitadel API running at http://localhost:8080.
+To change this, export the link to your environment.json in your environment variables.
+
+```bash
+export ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.us1.zitadel.cloud/ui/console/assets/environment.json
+```
+
+Proceed [with configuring your console redirect URIs](console-redirect).
+
+#### Develop against a local Zitadel instance from scratch
+
+By executing the commands from this section, you run everything you need to develop the console locally.
+Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](https://www.postgresql.org/download/) and the [latest release of Zitadel](https://github.com/zitadel/zitadel/releases/latest) on your local machine.
+You use the Zitadel container as backend for your console.
+
Run the database and the latest backend locally.
```bash
-# Change to the console directory
-cd ./console
-
+# Start from the root of the repository
# You just need the db and the zitadel services to develop the console against.
-docker compose --file ../e2e/docker-compose.yaml up --detach zitadel
+docker compose --file ./e2e/docker-compose.yaml up --detach zitadel
```
-When the backend is ready, you have the latest zitadel exposed at http://localhost:8080.
-You can now run a local development server with live code reloading at http://localhost:4200.
-To allow console access via http://localhost:4200, you have to configure the ZITADEL backend.
+When Zitadel accepts traffic, navigate to http://localhost:8080/ui/console/projects?login_hint=zitadel-admin@zitadel.localhost and log in with _Password1!_.
-1. Navigate to .
-2. When prompted, login with _zitadel-admin@zitadel.localhost_ and _Password1!_
-3. Select the _ZITADEL_ project.
+Proceed [with configuring your console redirect URIs](console-redirect).
+
+#### Configure Console redirect URI
+
+To allow console access via http://localhost:4200, you have to configure the Zitadel backend.
+
+1. Navigate to /ui/console/projects in your target Zitadel instance.
+3. Select the _Zitadel_ project.
4. Select the _Console_ application.
5. Select _Redirect Settings_
6. Add _http://localhost:4200/auth/callback_ to the _Redirect URIs_
7. Add _http://localhost:4200/signedout_ to the _Post Logout URIs_
8. Select the _Save_ button
-You can run the local console development server now.
+#### Develop
+
+Run the local console development server.
```bash
-# Install npm dependencies
-yarn install
+# Install dependencies (from repository root)
+pnpm install
-# Generate source files from Protos
-yarn generate
+# Option 1: Run console development server with live reloading and dependency rebuilds
+pnpm turbo dev --filter=console
-# Start the server
-yarn start
-
-# If you don't want to develop against http://localhost:8080, you can use another environment
-ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.zitadel.cloud/ui/console/assets/environment.json yarn start
+# Option 2: Build and serve console (production build)
+pnpm turbo build --filter=console
+pnpm turbo serve --filter=console
```
Navigate to http://localhost:4200/.
Make some changes to the source code and see how the browser is automatically updated.
-After making changes to the code, you should run the end-to-end-tests.
-Open another shell.
+
+#### Pass Quality Checks
+
+Reproduce the pipelines linting and testing for the console.
```bash
-# Reformat your console code
-yarn lint:fix
-
-# Change to the e2e directory
-cd .. && cd e2e/
-
-# If you made changes in the e2e directory, make sure you reformat the files here too
-npm run lint:fix
-
-# Install npm dependencies
-npm install
-
-# Run all e2e tests
-npm run e2e:angular -- --headed
+pnpm turbo quality --filter=console --filter=e2e
```
-You can also open the test suite interactively for fast feedback on specific tests.
+Fix the [quality checks](troubleshoot-frontend), add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass.
+
+### Contribute to Docs
+
+Project documentation is made with Docusaurus and is located under [./docs](./docs). The documentation uses **pnpm** and **Turbo** for development and build processes.
+
+#### Local Development
```bash
-# Run tests interactively
-npm run open:angular
+# Install dependencies (from repository root)
+pnpm install
+
+# Option 1: Run docs development server with Turbo (recommended)
+pnpm turbo dev --filter=zitadel-docs
+
+# Option 2: Build and serve docs (production build)
+pnpm turbo build --filter=zitadel-docs
+cd ./docs && pnpm serve
```
-If you also make [changes to the backend code](#backend--login), you can run the test against your locally built backend code and frontend server
+The docs build process automatically:
-```bash
-npm run open:golangangular
-npm run e2e:golangangular
-```
+1. Downloads required protoc plugins
+2. Generates gRPC documentation from proto files
+3. Generates API documentation from OpenAPI specs
+4. Copies configuration files
+5. Builds the Docusaurus site
-When you are happy with your changes, you can format your code and cleanup your environment
+#### Local testing
-```bash
-# Stop and remove the docker containers for zitadel and the database
-docker compose down
-```
+The documentation server will be available at http://localhost:3000 with live reload for fast development feedback.
-## Contribute docs
-
-Project documentation is made with docusaurus and is located under [./docs](./docs).
-
-### Local testing
-
-Please refer to the [README](./docs/README.md) for more information and local testing.
-
-### Style guide
+#### Style guide
- **Code with variables**: Make sure that code snippets can be used by setting environment variables, instead of manually replacing a placeholder.
- **Embedded files**: When embedding mdx files, make sure the template ist prefixed by "\_" (lowdash). The content will be rendered inside the parent page, but is not accessible individually (eg, by search).
@@ -431,14 +543,54 @@ The style guide covers a lot of material, so their [highlights](https://develope
- Use active voice: make clear who's performing the action.
- Use descriptive link text.
-### Docs pull request
+#### Docs pull request
When making a pull request use `docs(): ` as title for the semantic release.
Scope can be left empty (omit the brackets) or refer to the top navigation sections.
-## Contribute internationalization
+#### Pass Quality Checks
-ZITADEL loads translations from four files:
+Reproduce the pipelines linting checks for the docs.
+
+```bash
+pnpm turbo quality --filter=docs
+```
+
+Fix the [quality checks](troubleshoot-frontend), add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass.
+
+### Troubleshoot Frontend Quality Checks
+
+To debug and fix failing tasks, execute them individually using the `--filter` flag.
+
+We recommend to use [one of the dev containers](dev-containers) to reproduce pipeline issues.
+
+```bash
+# to reproduce linting error in the console:
+pnpm lint --filter=console
+# To fix them:
+pnpm lint:fix --filter=console
+```
+
+More tasks that are runnable on-demand.
+Some tasks have variants like `pnpm test:e2e:angulargolang`,
+others support arguments and flags like `pnpm test:integration run --spec apps/login/integration/integration/login.cy.ts`.
+For the turbo commands, check your options with `pnpm turbo --help`
+
+| Command | Description | Example |
+| ------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `pnpm turbo run generate` | Generate stubs from Proto files | Generate API docs: `pnpm turbo run generate --filter zitadel-docs` |
+| `pnpm turbo build` | Build runnable JavaScript code | Regenerate the proto stubs and build the @zitadel/client package: `pnpm turbo build --filter @zitadel/client` |
+| `pnpm turbo quality` | Reproduce the pipeline quality checks | Run login-related quality checks `pnpm turbo quality --filter './apps/login/*' --filter './packages/*'` |
+| `pnpm turbo lint` | Check linting issues | Check login-related linting issues for differences with main `pnpm turbo lint --filter=[main...HEAD] --filter .'/apps/login/**/*' --filter './packages/*'` |
+| `pnpm turbo lint:fix` | Fix linting issues | Fix console-relevant linting issues `pnpm turbo lint:fix --filter console --filter './packages/*' --filter zitadel-e2e` |
+| `pnpm turbo test:unit` | Run unit tests. Rerun on file changes | Run unit tests in all packages in and watch for file changes `pnpm turbo watch test:unit` |
+| `pnpm turbo test:e2e` | Run the Cypress CLI for console e2e tests | Test interactively against the console in a local dev server and Zitadel in a container: `pnpm turbo test:e2e:angular open` |
+| `pnpm turbo down` | Remove containers and volumes | Shut down containers from the integration test setup `pnpm turbo down` |
+| `pnpm turbo clean` | Remove downloaded dependencies and other generated files | Remove generated docs `pnpm turbo clean --filter zitadel-docs` |
+
+## Contribute Translations
+
+Zitadel loads translations from four files:
- [Console texts](./console/src/assets/i18n)
- [Login interface](./internal/api/ui/login/static/i18n)
@@ -449,13 +601,14 @@ You may edit the texts in these files or create a new file for additional langua
Please make sure that the languages within the files remain in their own language, e.g. German must always be `Deutsch.
If you have added support for a new language, please also ensure that it is added in the list of languages in all the other language files.
-You also have to add some changes to the following files:
+You also have to add some changes to the following files:
+
- [Register Local File](./console/src/app/app.module.ts)
- [Add Supported Language](./console/src/app/utils/language.ts)
- [Customized Text Docs](./docs/docs/guides/manage/customize/texts.md)
- [Add language option](./internal/api/ui/login/static/templates/external_not_found_option.html)
-## Want to start ZITADEL?
+## Want to start Zitadel?
You can find an installation guide for all the different environments here:
[https://zitadel.com/docs/self-hosting/deploy/overview](https://zitadel.com/docs/self-hosting/deploy/overview)
@@ -466,14 +619,14 @@ You can find an installation guide for all the different environments here:
## Product management
-The ZITADEL Team works with an agile product management methodology.
+The Zitadel Team works with an agile product management methodology.
You can find all the issues prioritized and ordered in the [product board](https://github.com/orgs/zitadel/projects/2/views/1).
### Sprint
We want to deliver a new release every second week. So we plan everything in two-week sprints.
Each Tuesday we estimate new issues and on Wednesday the last sprint will be reviewed and the next one will be planned.
-After a sprint ends a new version of ZITADEL will be released, and publish to [ZITADEL Cloud](https://zitadel.cloud) the following Monday.
+After a sprint ends a new version of Zitadel will be released, and publish to [Zitadel Cloud](https://zitadel.cloud) the following Monday.
If there are some critical or urgent issues we will have a look at it earlier, than the two weeks.
To show the community the needed information, each issue gets attributes and labels.
@@ -493,15 +646,16 @@ The state should reflect the progress of the issue and what is going on right no
- **🔖 Ready**: The issue is ready to take into a sprint. Difference to "prioritized..." is that the complexity is defined by the team.
- **📋 Sprint backlog**: The issue is scheduled for the current sprint.
- **🏗 In progress**: Someone is working on this issue right now. The issue will get an assignee as soon as it is in progress.
+- **❌ Blocked**: The issue is blocked until another issue is resolved/done.
- **👀 In review**: The issue is in review. Please add someone to review your issue or let us know that it is ready to review with a comment on your pull request.
- **✅ Done**: The issue is implemented and merged to main.
#### Priority
-Priority shows you the priority the ZITADEL team has given this issue. In general the higher the demand from customers and community for the feature, the higher the priority.
+Priority shows you the priority the Zitadel team has given this issue. In general the higher the demand from customers and community for the feature, the higher the priority.
- **🌋 Critical**: This is a security issue or something that has to be fixed urgently, because the software is not usable or highly vulnerable.
-- **🏔 High**: These are the issues the ZITADEL team is currently focusing on and will be implemented as soon as possible.
+- **🏔 High**: These are the issues the Zitadel team is currently focusing on and will be implemented as soon as possible.
- **🏕 Medium**: After all the high issues are done these will be next.
- **🏝 Low**: This is low in priority and will probably not be implemented in the next time or just if someone has some time in between.
@@ -516,18 +670,18 @@ Everything that is higher than 8 should be split in smaller parts.
There are a few general labels that don't belong to a specific category.
-- **good first issue**: This label shows contributors, that it is an easy entry point to start developing on ZITADEL.
-- **help wanted**: The author is seeking help on this topic, this may be from an internal ZITADEL team member or external contributors.
+- **good first issue**: This label shows contributors, that it is an easy entry point to start developing on Zitadel.
+- **help wanted**: The author is seeking help on this topic, this may be from an internal Zitadel team member or external contributors.
#### Category
-The category shows which part of ZITADEL is affected.
+The category shows which part of Zitadel is affected.
- **category: backend**: The backend includes the APIs, event store, command and query side. This is developed in golang.
- **category: ci**: ci is all about continues integration and pipelines.
-- **category: design**: All about the ux/ui of ZITADEL
+- **category: design**: All about the ux/ui of Zitadel
- **category: docs**: Adjustments or new documentations, this can be found in the docs folder.
-- **category: frontend**: The frontend concerns on the one hand the ZITADEL management console (Angular) and on the other hand the login (gohtml)
+- **category: frontend**: The frontend concerns on the one hand the Zitadel management console (Angular) and on the other hand the login (gohtml)
- **category: infra**: Infrastructure does include many different parts. E.g Terraform-provider, docker, metrics, etc.
- **category: translation**: Everything concerning translations or new languages
diff --git a/LICENSING.md b/LICENSING.md
index 9cad2082f8..ca4717afa5 100644
--- a/LICENSING.md
+++ b/LICENSING.md
@@ -18,6 +18,14 @@ The following files and directories, including their subdirectories, are license
proto/
```
+
+The following files and directories, including their subdirectories, are licensed under the [MIT License](https://opensource.org/license/mit/):
+
+```
+login/
+clients/
+```
+
## Community Contributions
To maintain a clear licensing structure and facilitate community contributions, all contributions must be licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) to be accepted. By submitting a contribution, you agree to this licensing.
diff --git a/Makefile b/Makefile
index 3c50231bee..ad561ab725 100644
--- a/Makefile
+++ b/Makefile
@@ -12,12 +12,22 @@ ZITADEL_MASTERKEY ?= MasterkeyNeedsToHave32Characters
export GOCOVERDIR ZITADEL_MASTERKEY
+LOGIN_REMOTE_NAME := login
+LOGIN_REMOTE_URL ?= https://github.com/zitadel/typescript.git
+LOGIN_REMOTE_BRANCH ?= main
+
.PHONY: compile
compile: core_build console_build compile_pipeline
.PHONY: docker_image
-docker_image: compile
- DOCKER_BUILDKIT=1 docker build -f build/Dockerfile -t $(ZITADEL_IMAGE) .
+docker_image:
+ @if [ ! -f ./zitadel ]; then \
+ echo "Compiling zitadel binary"; \
+ $(MAKE) compile; \
+ else \
+ echo "Reusing precompiled zitadel binary"; \
+ fi
+ DOCKER_BUILDKIT=1 docker build -f build/zitadel/Dockerfile -t $(ZITADEL_IMAGE) .
.PHONY: compile_pipeline
compile_pipeline: console_move
@@ -68,12 +78,13 @@ core_grpc_dependencies:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.22.0 # https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2?tab=versions
go install github.com/envoyproxy/protoc-gen-validate@v1.1.0 # https://pkg.go.dev/github.com/envoyproxy/protoc-gen-validate?tab=versions
go install github.com/bufbuild/buf/cmd/buf@v1.45.0 # https://pkg.go.dev/github.com/bufbuild/buf/cmd/buf?tab=versions
+ go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v1.18.1 # https://pkg.go.dev/connectrpc.com/connect/cmd/protoc-gen-connect-go?tab=versions
.PHONY: core_api
core_api: core_api_generator core_grpc_dependencies
buf generate
mkdir -p pkg/grpc
- cp -r .artifacts/grpc/github.com/zitadel/zitadel/pkg/grpc/* pkg/grpc/
+ cp -r .artifacts/grpc/github.com/zitadel/zitadel/pkg/grpc/** pkg/grpc/
mkdir -p openapi/v2/zitadel
cp -r .artifacts/grpc/zitadel/ openapi/v2/zitadel
@@ -86,18 +97,11 @@ console_move:
.PHONY: console_dependencies
console_dependencies:
- cd console && \
- yarn install --immutable
-
-.PHONY: console_client
-console_client:
- cd console && \
- yarn generate
+ npx pnpm install --frozen-lockfile --filter=./console
.PHONY: console_build
-console_build: console_dependencies console_client
- cd console && \
- yarn build
+console_build: console_dependencies
+ npx pnpm turbo build --filter=./console
.PHONY: clean
clean:
@@ -155,8 +159,7 @@ core_integration_test: core_integration_server_start core_integration_test_packa
.PHONY: console_lint
console_lint:
- cd console && \
- yarn lint
+ npx pnpm turbo lint --filter=./console
.PHONY: core_lint
core_lint:
@@ -165,3 +168,29 @@ core_lint:
--config ./.golangci.yaml \
--out-format=github-actions \
--concurrency=$$(getconf _NPROCESSORS_ONLN)
+
+.PHONY: login_pull
+login_pull: login_ensure_remote
+ @echo "Pulling changes from the 'apps/login' subtree on remote $(LOGIN_REMOTE_NAME) branch $(LOGIN_REMOTE_BRANCH)"
+ git fetch $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_BRANCH)
+ git merge -s ours --allow-unrelated-histories $(LOGIN_REMOTE_NAME)/$(LOGIN_REMOTE_BRANCH) -m "Synthetic merge to align histories"
+ git push
+
+.PHONY: login_push
+login_push: login_ensure_remote
+ @echo "Pushing changes to the 'apps/login' subtree on remote $(LOGIN_REMOTE_NAME) branch $(LOGIN_REMOTE_BRANCH)"
+ git subtree split --prefix=apps/login -b login-sync-tmp
+ git checkout login-sync-tmp
+ git fetch $(LOGIN_REMOTE_NAME) main
+ git merge -s ours --allow-unrelated-histories $(LOGIN_REMOTE_NAME)/main -m "Synthetic merge to align histories"
+ git push $(LOGIN_REMOTE_NAME) login-sync-tmp:$(LOGIN_REMOTE_BRANCH)
+ git checkout -
+ git branch -D login-sync-tmp
+
+login_ensure_remote:
+ @if ! git remote get-url $(LOGIN_REMOTE_NAME) > /dev/null 2>&1; then \
+ echo "Adding remote $(LOGIN_REMOTE_NAME)"; \
+ git remote add $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_URL); \
+ else \
+ echo "Remote $(LOGIN_REMOTE_NAME) already exists."; \
+ fi
diff --git a/apps/login/.dockerignore b/apps/login/.dockerignore
new file mode 100644
index 0000000000..4e64fd495f
--- /dev/null
+++ b/apps/login/.dockerignore
@@ -0,0 +1,21 @@
+*
+
+!constants
+!scripts
+!src
+!public
+!locales
+!next.config.mjs
+!next-env-vars.d.ts
+!next-env.d.ts
+!tailwind.config.js
+!tsconfig.json
+!package.json
+!pnpm-lock.yaml
+
+**/*.md
+**/*.png
+**/node_modules
+**/.turbo
+**/*.test.ts
+**/*.test.tsx
\ No newline at end of file
diff --git a/apps/login/.env.test b/apps/login/.env.test
new file mode 100644
index 0000000000..134fdb5669
--- /dev/null
+++ b/apps/login/.env.test
@@ -0,0 +1,5 @@
+NEXT_PUBLIC_BASE_PATH="/ui/v2/login"
+ZITADEL_API_URL=http://mock-zitadel:22222
+ZITADEL_SERVICE_USER_TOKEN="yolo"
+EMAIL_VERIFICATION=true
+DEBUG=true
diff --git a/apps/login/.eslintrc.cjs b/apps/login/.eslintrc.cjs
new file mode 100644
index 0000000000..d704a7f0c3
--- /dev/null
+++ b/apps/login/.eslintrc.cjs
@@ -0,0 +1,24 @@
+module.exports = {
+ parser: "@typescript-eslint/parser",
+ extends: ["next", "prettier"],
+ plugins: ["@typescript-eslint"],
+ rules: {
+ "@next/next/no-html-link-for-pages": "off",
+ "@next/next/no-img-element": "off",
+ "react/no-unescaped-entities": "off",
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": ["error", {
+ argsIgnorePattern: "^_" ,
+ varsIgnorePattern: "^_" ,
+ }],
+ "no-undef": "off",
+ },
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ project: "./tsconfig.json",
+ },
+};
diff --git a/apps/login/.github/ISSUE_TEMPLATE/bug.yaml b/apps/login/.github/ISSUE_TEMPLATE/bug.yaml
new file mode 100644
index 0000000000..2764c1a365
--- /dev/null
+++ b/apps/login/.github/ISSUE_TEMPLATE/bug.yaml
@@ -0,0 +1,63 @@
+name: 🐛 Bug Report
+description: "Create a bug report to help us improve ZITADEL Typescript Library."
+title: "[Bug]: "
+labels: ["bug"]
+body:
+- type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+- type: checkboxes
+ id: preflight
+ attributes:
+ label: Preflight Checklist
+ options:
+ - label:
+ I could not find a solution in the documentation, the existing issues or discussions
+ required: true
+ - label:
+ I have joined the [ZITADEL chat](https://zitadel.com/chat)
+ validations:
+ required: true
+- type: input
+ id: version
+ attributes:
+ label: Version
+ description: Which version of ZITADEL Typescript Library are you using.
+- type: textarea
+ id: impact
+ attributes:
+ label: Describe the problem caused by this bug
+ description: A clear and concise description of the problem you have and what the bug is.
+ validations:
+ required: true
+- type: textarea
+ id: reproduce
+ attributes:
+ label: To reproduce
+ description: Steps to reproduce the behaviour
+ placeholder: |
+ Steps to reproduce the behavior:
+ 1. ...
+ validations:
+ required: true
+- type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots
+ description: If applicable, add screenshots to help explain your problem.
+- type: textarea
+ id: expected
+ attributes:
+ label: Expected behavior
+ description: A clear and concise description of what you expected to happen.
+- type: textarea
+ id: config
+ attributes:
+ label: Relevant Configuration
+ description: Add any relevant configurations that could help us. Make sure to redact any sensitive information.
+- type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Please add any other infos that could be useful.
diff --git a/apps/login/.github/ISSUE_TEMPLATE/config.yml b/apps/login/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..7e690b9344
--- /dev/null
+++ b/apps/login/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,4 @@
+blank_issues_enabled: true
+contact_links:
+ - name: 💬 ZITADEL Community Chat
+ url: https://zitadel.com/chat
diff --git a/apps/login/.github/ISSUE_TEMPLATE/docs.yaml b/apps/login/.github/ISSUE_TEMPLATE/docs.yaml
new file mode 100644
index 0000000000..04c1c0cdb1
--- /dev/null
+++ b/apps/login/.github/ISSUE_TEMPLATE/docs.yaml
@@ -0,0 +1,30 @@
+name: 📄 Documentation
+description: Create an issue for missing or wrong documentation.
+labels: ["docs"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this issue.
+ - type: checkboxes
+ id: preflight
+ attributes:
+ label: Preflight Checklist
+ options:
+ - label:
+ I could not find a solution in the existing issues, docs, nor discussions
+ required: true
+ - label:
+ I have joined the [ZITADEL chat](https://zitadel.com/chat)
+ - type: textarea
+ id: docs
+ attributes:
+ label: Describe the docs your are missing or that are wrong
+ placeholder: As a [type of user], I want [some goal] so that [some reason].
+ validations:
+ required: true
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Please add any other infos that could be useful.
diff --git a/apps/login/.github/ISSUE_TEMPLATE/improvement.yaml b/apps/login/.github/ISSUE_TEMPLATE/improvement.yaml
new file mode 100644
index 0000000000..cfe79d407b
--- /dev/null
+++ b/apps/login/.github/ISSUE_TEMPLATE/improvement.yaml
@@ -0,0 +1,54 @@
+name: 🛠️ Improvement
+description: "Create an new issue for an improvment in ZITADEL"
+labels: ["improvement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this improvement request
+ - type: checkboxes
+ id: preflight
+ attributes:
+ label: Preflight Checklist
+ options:
+ - label:
+ I could not find a solution in the existing issues, docs, nor discussions
+ required: true
+ - label:
+ I have joined the [ZITADEL chat](https://zitadel.com/chat)
+ - type: textarea
+ id: problem
+ attributes:
+ label: Describe your problem
+ description: Please describe your problem this improvement is supposed to solve.
+ placeholder: Describe the problem you have
+ validations:
+ required: true
+ - type: textarea
+ id: solution
+ attributes:
+ label: Describe your ideal solution
+ description: Which solution do you propose?
+ placeholder: As a [type of user], I want [some goal] so that [some reason].
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: Version
+ description: Which version of the typescript library are you using.
+ - type: dropdown
+ id: environment
+ attributes:
+ label: Environment
+ description: How do you use ZITADEL?
+ options:
+ - ZITADEL Cloud
+ - Self-hosted
+ validations:
+ required: true
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Please add any other infos that could be useful.
diff --git a/apps/login/.github/ISSUE_TEMPLATE/proposal.yaml b/apps/login/.github/ISSUE_TEMPLATE/proposal.yaml
new file mode 100644
index 0000000000..cd9ff66972
--- /dev/null
+++ b/apps/login/.github/ISSUE_TEMPLATE/proposal.yaml
@@ -0,0 +1,54 @@
+name: 💡 Proposal / Feature request
+description: "Create an issue for a feature request/proposal."
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this proposal / feature reqeust
+ - type: checkboxes
+ id: preflight
+ attributes:
+ label: Preflight Checklist
+ options:
+ - label:
+ I could not find a solution in the existing issues, docs, nor discussions
+ required: true
+ - label:
+ I have joined the [ZITADEL chat](https://zitadel.com/chat)
+ - type: textarea
+ id: problem
+ attributes:
+ label: Describe your problem
+ description: Please describe your problem this proposal / feature is supposed to solve.
+ placeholder: Describe the problem you have.
+ validations:
+ required: true
+ - type: textarea
+ id: solution
+ attributes:
+ label: Describe your ideal solution
+ description: Which solution do you propose?
+ placeholder: As a [type of user], I want [some goal] so that [some reason].
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: Version
+ description: Which version of the Typescript Library are you using.
+ - type: dropdown
+ id: environment
+ attributes:
+ label: Environment
+ description: How do you use ZITADEL?
+ options:
+ - ZITADEL Cloud
+ - Self-hosted
+ validations:
+ required: true
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Please add any other infos that could be useful.
diff --git a/apps/login/.github/custom-i18n.png b/apps/login/.github/custom-i18n.png
new file mode 100644
index 0000000000..2306e62f87
Binary files /dev/null and b/apps/login/.github/custom-i18n.png differ
diff --git a/apps/login/.github/dependabot.example.yml b/apps/login/.github/dependabot.example.yml
new file mode 100644
index 0000000000..8f3906c179
--- /dev/null
+++ b/apps/login/.github/dependabot.example.yml
@@ -0,0 +1,21 @@
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: '/'
+ open-pull-requests-limit: 1
+ schedule:
+ interval: 'daily'
+
+ - package-ecosystem: npm
+ directory: '/'
+ open-pull-requests-limit: 3
+ schedule:
+ interval: daily
+ groups:
+ prod:
+ dependency-type: production
+ dev:
+ dependency-type: development
+ ignore:
+ - dependency-name: "eslint"
+ versions: [ "9.x" ]
diff --git a/apps/login/.github/pull_request_template.md b/apps/login/.github/pull_request_template.md
new file mode 100644
index 0000000000..138d4919af
--- /dev/null
+++ b/apps/login/.github/pull_request_template.md
@@ -0,0 +1,13 @@
+### Definition of Ready
+
+- [ ] I am happy with the code
+- [ ] Short description of the feature/issue is added in the pr description
+- [ ] PR is linked to the corresponding user story
+- [ ] Acceptance criteria are met
+- [ ] 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 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/apps/login/.github/workflows/close_pr.yml b/apps/login/.github/workflows/close_pr.yml
new file mode 100644
index 0000000000..90f92eff55
--- /dev/null
+++ b/apps/login/.github/workflows/close_pr.yml
@@ -0,0 +1,39 @@
+name: Auto-close PRs and guide to correct repo
+
+on:
+ workflow_dispatch:
+ pull_request_target:
+ types: [opened]
+
+jobs:
+ auto-close:
+ runs-on: ubuntu-latest
+ if: github.repository_id == '622995060' && github.ref_name != 'mirror-zitadel-repo'
+ steps:
+ - name: Comment and close PR
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const message = `
+ 👋 **Thanks for your contribution @${{ github.event.pull_request.user.login }}!**
+
+ This repository \`${{ github.repository }}\` is a read-only mirror of the git subtree at [\`zitadel/zitadel/login\`](https://github.com/zitadel/zitadel).
+ Therefore, we close this pull request automatically.
+
+ Your changes are not lost. Submitting them to the main repository is easy:
+ 1. [Fork zitadel/zitadel](https://github.com/zitadel/zitadel/fork)
+ 2. Clone your Zitadel fork \`git clone https://github.com//zitadel.git\`
+ 3. Change directory to your Zitadel forks root.
+ 4. Pull your changes into the Zitadel fork by running \`make login_pull LOGIN_REMOTE_URL=https://github.com//typescript.git LOGIN_REMOTE_BRANCH=\`.
+ 5. Push your changes and [open a pull request to zitadel/zitadel](https://github.com/zitadel/zitadel/compare)
+ `.trim();
+ await github.rest.issues.createComment({
+ ...context.repo,
+ issue_number: context.issue.number,
+ body: message
+ });
+ await github.rest.pulls.update({
+ ...context.repo,
+ pull_number: context.issue.number,
+ state: "closed"
+ });
diff --git a/apps/login/.github/workflows/issues.yml b/apps/login/.github/workflows/issues.yml
new file mode 100644
index 0000000000..ff12b8fe04
--- /dev/null
+++ b/apps/login/.github/workflows/issues.yml
@@ -0,0 +1,41 @@
+name: Add new issues to product management project
+
+on:
+ issues:
+ types:
+ - opened
+
+jobs:
+ add-to-project:
+ name: Add issue and community pr to project
+ runs-on: ubuntu-latest
+ if: github.repository_id == '622995060'
+ steps:
+ - name: add issue
+ uses: actions/add-to-project@v1.0.2
+ if: ${{ github.event_name == 'issues' }}
+ with:
+ # You can target a repository in a different organization
+ # to the issue
+ project-url: https://github.com/orgs/zitadel/projects/2
+ github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
+ - uses: tspascoal/get-user-teams-membership@v3
+ id: checkUserMember
+ if: github.actor != 'dependabot[bot]'
+ with:
+ username: ${{ github.actor }}
+ GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }}
+ - name: add pr
+ uses: actions/add-to-project@v1.0.2
+ if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}}
+ with:
+ # You can target a repository in a different organization
+ # to the issue
+ project-url: https://github.com/orgs/zitadel/projects/2
+ github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
+ - uses: actions-ecosystem/action-add-labels@v1.1.3
+ if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}}
+ with:
+ github_token: ${{ secrets.ADD_TO_PROJECT_PAT }}
+ labels: |
+ os-contribution
diff --git a/apps/login/.github/workflows/release.yml b/apps/login/.github/workflows/release.yml
new file mode 100644
index 0000000000..2508627d1b
--- /dev/null
+++ b/apps/login/.github/workflows/release.yml
@@ -0,0 +1,32 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main
+
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ if: github.repository_id != '622995060'
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Create Release Pull Request
+ uses: changesets/action@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/apps/login/.github/workflows/test.yml b/apps/login/.github/workflows/test.yml
new file mode 100644
index 0000000000..7b4721dbee
--- /dev/null
+++ b/apps/login/.github/workflows/test.yml
@@ -0,0 +1,67 @@
+name: Quality
+on:
+ pull_request:
+ workflow_dispatch:
+ inputs:
+ ignore-run-cache:
+ description: 'Whether to ignore the run cache'
+ required: false
+ default: true
+ ref-tag:
+ description: 'overwrite the DOCKER_METADATA_OUTPUT_VERSION environment variable used by the make file'
+ required: false
+ default: ''
+jobs:
+ quality:
+ name: Ensure Quality
+ if: github.event_name == 'workflow_dispatch' ||
+ (github.event_name == 'pull_request' && github.repository_id != '622995060')
+ runs-on: ubuntu-22.04
+ timeout-minutes: 30
+ permissions:
+ contents: read # We only need read access to the repository contents
+ actions: write # We need write access to the actions cache
+ env:
+ CACHE_DIR: /tmp/login-run-caches
+ # Only run this job on workflow_dispatch or pushes to forks
+ steps:
+ - uses: actions/checkout@v4
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ ghcr.io/zitadel/login
+ tags: |
+ type=raw,value=latest,enable={{is_default_branch}}
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+ - name: Set up Buildx
+ uses: docker/setup-buildx-action@v3
+ # Only with correctly restored build cache layers, the run caches work as expected.
+ # To restore docker build layer caches, extend the docker-bake.hcl to use the cache-from and cache-to options.
+ # https://docs.docker.com/build/ci/github-actions/cache/
+ # Alternatively, you can use a self-hosted runner or a third-party builder that restores build layer caches out-of-the-box, like https://depot.dev/
+ - name: Restore Run Caches
+ uses: actions/cache/restore@v4
+ id: run-caches-restore
+ with:
+ path: ${{ env.CACHE_DIR }}
+ key: ${{ runner.os }}-login-run-caches-${{github.ref_name}}-${{ github.sha }}-${{github.run_attempt}}
+ restore-keys: |
+ ${{ runner.os }}-login-run-caches-${{github.ref_name}}-${{ github.sha }}-
+ ${{ runner.os }}-login-run-caches-${{github.ref_name}}-
+ ${{ runner.os }}-login-run-caches-
+ - run: make login_quality
+ env:
+ IGNORE_RUN_CACHE: ${{ github.event.inputs.ignore-run-cache == 'true' }}
+ DOCKER_METADATA_OUTPUT_VERSION: ${{ github.event.inputs.ref-tag || env.DOCKER_METADATA_OUTPUT_VERSION || steps.meta.outputs.version }}
+ - name: Save Run Caches
+ uses: actions/cache/save@v4
+ with:
+ path: ${{ env.CACHE_DIR }}
+ key: ${{ steps.run-caches-restore.outputs.cache-primary-key }}
+ if: always()
diff --git a/apps/login/.gitignore b/apps/login/.gitignore
new file mode 100644
index 0000000000..17a18bf973
--- /dev/null
+++ b/apps/login/.gitignore
@@ -0,0 +1,16 @@
+custom-config.js
+.env*.local
+standalone
+tsconfig.tsbuildinfo
+
+.DS_Store
+node_modules
+.turbo
+*.log
+.next
+dist
+dist-ssr
+*.local
+.env
+.vscode
+/blob-report/
diff --git a/apps/login/.prettierignore b/apps/login/.prettierignore
new file mode 100644
index 0000000000..413c4b52e0
--- /dev/null
+++ b/apps/login/.prettierignore
@@ -0,0 +1,5 @@
+*
+!constants
+!src
+!locales
+!scripts/healthcheck.js
\ No newline at end of file
diff --git a/apps/login/.prettierrc b/apps/login/.prettierrc
new file mode 100644
index 0000000000..ba42405b03
--- /dev/null
+++ b/apps/login/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "printWidth": 125,
+ "trailingComma": "all",
+ "plugins": ["prettier-plugin-organize-imports"],
+ "filepath": ""
+}
diff --git a/apps/login/CODE_OF_CONDUCT.md b/apps/login/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..ac3f129652
--- /dev/null
+++ b/apps/login/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+legal@zitadel.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/apps/login/CONTRIBUTING.md b/apps/login/CONTRIBUTING.md
new file mode 100644
index 0000000000..50ca3172d1
--- /dev/null
+++ b/apps/login/CONTRIBUTING.md
@@ -0,0 +1,218 @@
+# Contributing
+
+:attention: In this CONTRIBUTING.md you read about contributing to this very repository.
+If you want to develop your own login UI, please refer [to the README.md](./README.md).
+
+## Introduction
+
+Thank you for your interest about how to contribute!
+
+:attention: If you notice a possible **security vulnerability**, please don't hesitate to disclose any concern by contacting [security@zitadel.com](mailto:security@zitadel.com).
+You don't have to be perfectly sure about the nature of the vulnerability.
+We will give them a high priority and figure them out.
+
+We also appreciate all your other ideas, thoughts and feedback and will take care of them as soon as possible.
+We love to discuss in an open space using [GitHub issues](https://github.com/zitadel/typescript/issues),
+[GitHub discussions in the core repo](https://github.com/zitadel/zitadel/discussions)
+or in our [chat on Discord](https://zitadel.com/chat).
+For private discussions,
+you have [more contact options on our Website](https://zitadel.com/contact).
+
+## Pull Requests
+
+The repository zitadel/typescript is a read-only mirror of the git subtree at zitadel/zitadel/login.
+To submit changes, please open a Pull Request [in the zitadel/zitadel repository](https://github.com/zitadel/zitadel/compare).
+
+If you already made changes based on the zitadel/typescript repository, these changes are not lost.
+Submitting them to the main repository is easy:
+
+1. [Fork zitadel/zitadel](https://github.com/zitadel/zitadel/fork)
+1. Clone your Zitadel fork git clone https://github.com//zitadel.git
+1. Change directory to your Zitadel forks root.
+1. Pull your changes into the Zitadel fork by running make login_pull LOGIN_REMOTE_URL=https://github.com//typescript.git LOGIN_REMOTE_BRANCH=.
+1. Push your changes and [open a pull request to zitadel/zitadel](https://github.com/zitadel/zitadel/compare)
+
+Please consider the following guidelines when creating a pull request.
+
+- The latest changes are always in `main`, so please make your pull request against that branch.
+- pull requests should be raised for any change
+- Pull requests need approval of a Zitadel core engineer @zitadel/engineers before merging
+- We use ESLint/Prettier for linting/formatting, so please run `pnpm lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
+- If you add new functionality, please provide the corresponding documentation as well and make it part of the pull request
+
+### Setting up local environment
+
+```sh
+# Install dependencies. Developing requires Node.js v20
+pnpm install
+
+# Generate gRPC stubs
+pnpm generate
+
+# Start a local development server for the login and manually configure apps/login/.env.local
+pnpm dev
+```
+
+The application is now available at `http://localhost:3000`
+
+Configure apps/login/.env.local to target the Zitadel instance of your choice.
+The login app live-reloads on changes, so you can start developing right away.
+
+### Developing Against A Local Latest Zitadel Release
+
+The following command uses Docker to run a local Zitadel instance and the login application in live-reloading dev mode.
+Additionally, it runs a Traefik reverse proxy that exposes the login with a self-signed certificate at https://127.0.0.1.sslip.io
+127.0.0.1.sslip.io is a special domain that resolves to your localhost, so it's safe to allow your browser to proceed with loading the page.
+
+```sh
+# Install dependencies. Developing requires Node.js v20
+pnpm install
+
+# Generate gRPC stubs
+pnpm generate
+
+# Start a local development server and have apps/login/.env.test.local configured for you to target the local Zitadel instance.
+pnpm dev:local
+```
+
+Log in at https://127.0.0.1.sslip.io/ui/v2/login/loginname and use the following credentials:
+**Loginname**: *zitadel-admin@zitadel.127.0.0.1.sslip.io*
+**Password**: _Password1!_.
+
+The login app live-reloads on changes, so you can start developing right away.
+
+### Developing Against A Locally Compiled Zitadel
+
+To develop against a locally compiled version of Zitadel, you need to build the Zitadel docker image first.
+Clone the [Zitadel repository](https://github.com/zitadel/zitadel.git) and run the following command from its root:
+
+```sh
+# This compiles a Zitadel binary if it does not exist at ./zitadel already and copies it into a Docker image.
+# If you want to recompile the binary, run `make compile` first
+make login_dev
+```
+
+Open another terminal session at zitadel/zitadel/login and run the following commands to start the dev server.
+
+```bash
+# Install dependencies. Developing requires Node.js v20
+pnpm install
+
+# Start a local development server and have apps/login/.env.test.local configured for you to target the local Zitadel instance.
+NODE_ENV=test pnpm dev
+```
+
+Log in at https://127.0.0.1.sslip.io/ui/v2/login/loginname and use the following credentials:
+**Loginname**: *zitadel-admin@zitadel.127.0.0.1.sslip.io*
+**Password**: _Password1!_.
+
+The login app live-reloads on changes, so you can start developing right away.
+
+### Quality Assurance
+
+Use `make` commands to test the quality of your code against a production build without installing any dependencies besides Docker.
+Using `make` commands, you can reproduce and debug the CI pipelines locally.
+
+```sh
+# Reproduce the whole CI pipeline in docker
+make login_quality
+# Show other options with make
+make help
+```
+
+Use `pnpm` commands to run the tests in dev mode with live reloading and debugging capabilities.
+
+#### Linting and formatting
+
+Check the formatting and linting of the code in docker
+
+```sh
+make login_lint
+```
+
+Check the linting of the code using pnpm
+
+```sh
+pnpm lint
+pnpm format
+```
+
+Fix the linting of your code
+
+```sh
+pnpm lint:fix
+pnpm format:fix
+```
+
+#### Running Unit Tests
+
+Run the tests in docker
+
+```sh
+make login_test_unit
+```
+
+Run unit tests with live-reloading
+
+```sh
+pnpm test:unit
+```
+
+#### Running Integration Tests
+
+Run the test in docker
+
+```sh
+make login_test_integration
+```
+
+Alternatively, run a live-reloading development server with an interactive Cypress test suite.
+First, set up your local test environment.
+
+```sh
+# Install dependencies. Developing requires Node.js v20
+pnpm install
+
+# Generate gRPC stubs
+pnpm generate
+
+# Start a local development server and use apps/login/.env.test to use the locally mocked Zitadel API.
+pnpm test:integration:setup
+```
+
+Now, in another terminal session, open the interactive Cypress integration test suite.
+
+```sh
+pnpm test:integration open
+```
+
+Show more options with Cypress
+
+```sh
+pnpm test:integration help
+```
+
+#### Running Acceptance Tests
+
+To run the tests in docker against the latest release of Zitadel, use the following command:
+
+:warning: The acceptance tests are not reliable at the moment :construction:
+
+```sh
+make login_test_acceptance
+```
+
+Alternatively, run can use a live-reloading development server with an interactive Playwright test suite.
+Set up your local environment by running the commands either for [developing against a local latest Zitadel release](latest) or for [developing against a locally compiled Zitadel](compiled).
+
+Now, in another terminal session, open the interactive Playwright acceptance test suite.
+
+```sh
+pnpm test:acceptance open
+```
+
+Show more options with Playwright
+
+```sh
+pnpm test:acceptance help
+```
diff --git a/apps/login/Dockerfile b/apps/login/Dockerfile
new file mode 100644
index 0000000000..7e3d8668d2
--- /dev/null
+++ b/apps/login/Dockerfile
@@ -0,0 +1,36 @@
+FROM node:20-alpine AS base
+
+FROM base AS build
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@9.1.2 --activate && \
+ apk update && apk add --no-cache && \
+ rm -rf /var/cache/apk/*
+WORKDIR /app
+COPY pnpm-lock.yaml ./
+RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --frozen-lockfile
+COPY package.json ./
+RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --prod
+COPY . .
+RUN pnpm build:login:standalone
+
+FROM scratch AS build-out
+COPY --from=build /app/.next/standalone /
+COPY --from=build /app/.next/static /.next/static
+COPY --from=build /app/public /public
+
+FROM base AS login-standalone
+WORKDIR /runtime
+RUN addgroup --system --gid 1001 nodejs && \
+ adduser --system --uid 1001 nextjs
+# If /.env-file/.env is mounted into the container, its variables are made available to the server before it starts up.
+RUN mkdir -p /.env-file && touch /.env-file/.env && chown -R nextjs:nodejs /.env-file
+COPY ./scripts/ ./
+COPY --chown=nextjs:nodejs --from=build-out / ./
+USER nextjs
+ENV HOSTNAME="0.0.0.0"
+ENV PORT=3000
+# TODO: Check healthy, not ready
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+CMD ["/bin/sh", "-c", "node ./healthcheck.js http://localhost:${PORT}/ui/v2/login/healthy"]
+ENTRYPOINT ["./entrypoint.sh"]
diff --git a/apps/login/LICENSE b/apps/login/LICENSE
new file mode 100644
index 0000000000..89f750f2ab
--- /dev/null
+++ b/apps/login/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 ZITADEL
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/login/acceptance/.eslintrc.cjs b/apps/login/acceptance/.eslintrc.cjs
new file mode 100644
index 0000000000..84711c5881
--- /dev/null
+++ b/apps/login/acceptance/.eslintrc.cjs
@@ -0,0 +1,10 @@
+module.exports = {
+ root: true,
+ // Use basic ESLint config since the login app has its own detailed config
+ extends: ["eslint:recommended"],
+ settings: {
+ next: {
+ rootDir: ["apps/*/"],
+ },
+ },
+};
diff --git a/apps/login/acceptance/.gitignore b/apps/login/acceptance/.gitignore
new file mode 100644
index 0000000000..6a7425e885
--- /dev/null
+++ b/apps/login/acceptance/.gitignore
@@ -0,0 +1 @@
+go-command
diff --git a/apps/login/acceptance/go-command.Dockerfile b/apps/login/acceptance/go-command.Dockerfile
new file mode 100644
index 0000000000..fafebd6f4d
--- /dev/null
+++ b/apps/login/acceptance/go-command.Dockerfile
@@ -0,0 +1,11 @@
+ARG LOGIN_TEST_ACCEPTANCE_GOLANG_TAG="golang:1.24-alpine"
+
+FROM ${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG}
+RUN apk add curl jq
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN go build -o /go-command .
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s \
+ CMD curl -f http://localhost:${PORT}/healthy || exit 1
+ENTRYPOINT [ "/go-command" ]
diff --git a/apps/login/acceptance/idp/oidc/go.mod b/apps/login/acceptance/idp/oidc/go.mod
new file mode 100644
index 0000000000..bc43390218
--- /dev/null
+++ b/apps/login/acceptance/idp/oidc/go.mod
@@ -0,0 +1,28 @@
+module github.com/zitadel/typescript/acceptance/idp/oidc
+
+go 1.24.1
+
+require github.com/zitadel/oidc/v3 v3.37.0
+
+require (
+ github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
+ github.com/go-chi/chi/v5 v5.2.2 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.5 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
+ github.com/muhlemmer/gu v0.3.1 // indirect
+ github.com/muhlemmer/httpforwarded v0.1.0 // indirect
+ github.com/rs/cors v1.11.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/zitadel/logging v0.6.2 // indirect
+ github.com/zitadel/schema v1.3.1 // indirect
+ go.opentelemetry.io/otel v1.29.0 // indirect
+ go.opentelemetry.io/otel/metric v1.29.0 // indirect
+ go.opentelemetry.io/otel/trace v1.29.0 // indirect
+ golang.org/x/crypto v0.35.0 // indirect
+ golang.org/x/oauth2 v0.28.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+)
diff --git a/apps/login/acceptance/idp/oidc/go.sum b/apps/login/acceptance/idp/oidc/go.sum
new file mode 100644
index 0000000000..23fd2b3384
--- /dev/null
+++ b/apps/login/acceptance/idp/oidc/go.sum
@@ -0,0 +1,71 @@
+github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
+github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
+github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
+github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
+github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
+github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
+github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
+github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
+github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8=
+github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw=
+github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
+github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
+go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/apps/login/acceptance/idp/oidc/main.go b/apps/login/acceptance/idp/oidc/main.go
new file mode 100644
index 0000000000..b04ac94234
--- /dev/null
+++ b/apps/login/acceptance/idp/oidc/main.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "log/slog"
+ "net/http"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/zitadel/oidc/v3/example/server/exampleop"
+ "github.com/zitadel/oidc/v3/example/server/storage"
+)
+
+func main() {
+ apiURL := os.Getenv("API_URL")
+ pat := readPAT(os.Getenv("PAT_FILE"))
+ domain := os.Getenv("API_DOMAIN")
+ schema := os.Getenv("SCHEMA")
+ host := os.Getenv("HOST")
+ port := os.Getenv("PORT")
+
+ logger := slog.New(
+ slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+ AddSource: true,
+ Level: slog.LevelDebug,
+ }),
+ )
+
+ issuer := fmt.Sprintf("%s://%s:%s/", schema, host, port)
+ redirectURI := fmt.Sprintf("%s/idps/callback", apiURL)
+
+ clientID := "web"
+ clientSecret := "secret"
+ storage.RegisterClients(
+ storage.WebClient(clientID, clientSecret, redirectURI),
+ )
+
+ storage := storage.NewStorage(storage.NewUserStore(issuer))
+ router := exampleop.SetupServer(issuer, storage, logger, false)
+
+ server := &http.Server{
+ Addr: ":" + port,
+ Handler: router,
+ }
+ go func() {
+ if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+ log.Fatalf("HTTP server error: %v", err)
+ }
+ log.Println("Stopped serving new connections.")
+ }()
+
+ createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret)
+
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+ <-sigChan
+
+ shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
+ defer shutdownRelease()
+
+ if err := server.Shutdown(shutdownCtx); err != nil {
+ log.Fatalf("HTTP shutdown error: %v", err)
+ }
+}
+
+func readPAT(path string) string {
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ pat, err := io.ReadAll(f)
+ if err != nil {
+ panic(err)
+ }
+ return strings.Trim(string(pat), "\n")
+}
+
+func createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret string) error {
+ idpID, err := CreateIDP(apiURL, pat, domain, issuer, clientID, clientSecret)
+ if err != nil {
+ return err
+ }
+ return ActivateIDP(apiURL, pat, domain, idpID)
+}
+
+type createIDP struct {
+ Name string `json:"name"`
+ Issuer string `json:"issuer"`
+ ClientId string `json:"clientId"`
+ ClientSecret string `json:"clientSecret"`
+ Scopes []string `json:"scopes"`
+ ProviderOptions providerOptions `json:"providerOptions"`
+ IsIdTokenMapping bool `json:"isIdTokenMapping"`
+ UsePkce bool `json:"usePkce"`
+}
+
+type providerOptions struct {
+ IsLinkingAllowed bool `json:"isLinkingAllowed"`
+ IsCreationAllowed bool `json:"isCreationAllowed"`
+ IsAutoCreation bool `json:"isAutoCreation"`
+ IsAutoUpdate bool `json:"isAutoUpdate"`
+ AutoLinking string `json:"autoLinking"`
+}
+
+type idp struct {
+ ID string `json:"id"`
+}
+
+func CreateIDP(apiURL, pat, domain string, issuer, clientID, clientSecret string) (string, error) {
+ createIDP := &createIDP{
+ Name: "OIDC",
+ Issuer: issuer,
+ ClientId: clientID,
+ ClientSecret: clientSecret,
+ Scopes: []string{"openid", "profile", "email"},
+ ProviderOptions: providerOptions{
+ IsLinkingAllowed: true,
+ IsCreationAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ AutoLinking: "AUTO_LINKING_OPTION_USERNAME",
+ },
+ IsIdTokenMapping: false,
+ UsePkce: false,
+ }
+
+ resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/generic_oidc", pat, domain, createIDP)
+ if err != nil {
+ return "", err
+ }
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ idp := new(idp)
+ if err := json.Unmarshal(data, idp); err != nil {
+ return "", err
+ }
+ return idp.ID, nil
+}
+
+type activateIDP struct {
+ IdpId string `json:"idpId"`
+}
+
+func ActivateIDP(apiURL, pat, domain string, idpID string) error {
+ activateIDP := &activateIDP{
+ IdpId: idpID,
+ }
+ _, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP)
+ return err
+}
+
+func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
+ if err != nil {
+ return nil, err
+ }
+ values := http.Header{}
+ values.Add("Authorization", "Bearer "+pat)
+ values.Add("x-forwarded-host", domain)
+ values.Add("Content-Type", "application/json")
+ req.Header = values
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/apps/login/acceptance/idp/saml/go.mod b/apps/login/acceptance/idp/saml/go.mod
new file mode 100644
index 0000000000..e73b4feb3b
--- /dev/null
+++ b/apps/login/acceptance/idp/saml/go.mod
@@ -0,0 +1,16 @@
+module github.com/zitadel/typescript/acceptance/idp/saml
+
+go 1.24.1
+
+require (
+ github.com/crewjam/saml v0.4.14
+ github.com/mattermost/xml-roundtrip-validator v0.1.0
+ github.com/zenazn/goji v1.0.1
+ golang.org/x/crypto v0.36.0
+)
+
+require (
+ github.com/beevik/etree v1.1.0 // indirect
+ github.com/jonboulle/clockwork v0.2.2 // indirect
+ github.com/russellhaering/goxmldsig v1.3.0 // indirect
+)
diff --git a/apps/login/acceptance/idp/saml/go.sum b/apps/login/acceptance/idp/saml/go.sum
new file mode 100644
index 0000000000..1208550f6e
--- /dev/null
+++ b/apps/login/acceptance/idp/saml/go.sum
@@ -0,0 +1,49 @@
+github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
+github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
+github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
+github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
+github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
+github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/apps/login/acceptance/idp/saml/main.go b/apps/login/acceptance/idp/saml/main.go
new file mode 100644
index 0000000000..059eab79e2
--- /dev/null
+++ b/apps/login/acceptance/idp/saml/main.go
@@ -0,0 +1,328 @@
+package main
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+ "encoding/xml"
+ "errors"
+ "io"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+
+ "github.com/crewjam/saml"
+ "github.com/crewjam/saml/logger"
+ "github.com/crewjam/saml/samlidp"
+ xrv "github.com/mattermost/xml-roundtrip-validator"
+ "github.com/zenazn/goji"
+ "github.com/zenazn/goji/bind"
+ "github.com/zenazn/goji/web"
+ "golang.org/x/crypto/bcrypt"
+)
+
+var key = func() crypto.PrivateKey {
+ b, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA0OhbMuizgtbFOfwbK7aURuXhZx6VRuAs3nNibiuifwCGz6u9
+yy7bOR0P+zqN0YkjxaokqFgra7rXKCdeABmoLqCC0U+cGmLNwPOOA0PaD5q5xKhQ
+4Me3rt/R9C4Ca6k3/OnkxnKwnogcsmdgs2l8liT3qVHP04Oc7Uymq2v09bGb6nPu
+fOrkXS9F6mSClxHG/q59AGOWsXK1xzIRV1eu8W2SNdyeFVU1JHiQe444xLoPul5t
+InWasKayFsPlJfWNc8EoU8COjNhfo/GovFTHVjh9oUR/gwEFVwifIHihRE0Hazn2
+EQSLaOr2LM0TsRsQroFjmwSGgI+X2bfbMTqWOQIDAQABAoIBAFWZwDTeESBdrLcT
+zHZe++cJLxE4AObn2LrWANEv5AeySYsyzjRBYObIN9IzrgTb8uJ900N/zVr5VkxH
+xUa5PKbOcowd2NMfBTw5EEnaNbILLm+coHdanrNzVu59I9TFpAFoPavrNt/e2hNo
+NMGPSdOkFi81LLl4xoadz/WR6O/7N2famM+0u7C2uBe+TrVwHyuqboYoidJDhO8M
+w4WlY9QgAUhkPyzZqrl+VfF1aDTGVf4LJgaVevfFCas8Ws6DQX5q4QdIoV6/0vXi
+B1M+aTnWjHuiIzjBMWhcYW2+I5zfwNWRXaxdlrYXRukGSdnyO+DH/FhHePJgmlkj
+NInADDkCgYEA6MEQFOFSCc/ELXYWgStsrtIlJUcsLdLBsy1ocyQa2lkVUw58TouW
+RciE6TjW9rp31pfQUnO2l6zOUC6LT9Jvlb9PSsyW+rvjtKB5PjJI6W0hjX41wEO6
+fshFELMJd9W+Ezao2AsP2hZJ8McCF8no9e00+G4xTAyxHsNI2AFTCQcCgYEA5cWZ
+JwNb4t7YeEajPt9xuYNUOQpjvQn1aGOV7KcwTx5ELP/Hzi723BxHs7GSdrLkkDmi
+Gpb+mfL4wxCt0fK0i8GFQsRn5eusyq9hLqP/bmjpHoXe/1uajFbE1fZQR+2LX05N
+3ATlKaH2hdfCJedFa4wf43+cl6Yhp6ZA0Yet1r8CgYEAwiu1j8W9G+RRA5/8/DtO
+yrUTOfsbFws4fpLGDTA0mq0whf6Soy/96C90+d9qLaC3srUpnG9eB0CpSOjbXXbv
+kdxseLkexwOR3bD2FHX8r4dUM2bzznZyEaxfOaQypN8SV5ME3l60Fbr8ajqLO288
+wlTmGM5Mn+YCqOg/T7wjGmcCgYBpzNfdl/VafOROVbBbhgXWtzsz3K3aYNiIjbp+
+MunStIwN8GUvcn6nEbqOaoiXcX4/TtpuxfJMLw4OvAJdtxUdeSmEee2heCijV6g3
+ErrOOy6EqH3rNWHvlxChuP50cFQJuYOueO6QggyCyruSOnDDuc0BM0SGq6+5g5s7
+H++S/wKBgQDIkqBtFr9UEf8d6JpkxS0RXDlhSMjkXmkQeKGFzdoJcYVFIwq8jTNB
+nJrVIGs3GcBkqGic+i7rTO1YPkquv4dUuiIn+vKZVoO6b54f+oPBXd4S0BnuEqFE
+rdKNuCZhiaE2XD9L/O9KP1fh5bfEcKwazQ23EvpJHBMm8BGC+/YZNw==
+-----END RSA PRIVATE KEY-----`))
+ k, _ := x509.ParsePKCS1PrivateKey(b.Bytes)
+ return k
+}()
+
+var cert = func() *x509.Certificate {
+ b, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE-----
+MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
+BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5
+NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8A
+hs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+a
+ucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWx
+m+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6
+D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURN
+B2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0O
+BBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56
+zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5
+pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uv
+NONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEf
+y/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL
+/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsb
+GFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTL
+UzreO96WzlBBMtY=
+-----END CERTIFICATE-----`))
+ c, _ := x509.ParseCertificate(b.Bytes)
+ return c
+}()
+
+// Example from https://github.com/crewjam/saml/blob/main/example/idp/idp.go
+func main() {
+ apiURL := os.Getenv("API_URL")
+ pat := readPAT(os.Getenv("PAT_FILE"))
+ domain := os.Getenv("API_DOMAIN")
+ schema := os.Getenv("SCHEMA")
+ host := os.Getenv("HOST")
+ port := os.Getenv("PORT")
+
+ baseURL, err := url.Parse(schema + "://" + host + ":" + port)
+ if err != nil {
+
+ panic(err)
+ }
+
+ idpServer, err := samlidp.New(samlidp.Options{
+ URL: *baseURL,
+ Logger: logger.DefaultLogger,
+ Key: key,
+ Certificate: cert,
+ Store: &samlidp.MemoryStore{},
+ })
+ if err != nil {
+
+ panic(err)
+ }
+
+ metadata, err := xml.MarshalIndent(idpServer.IDP.Metadata(), "", " ")
+ if err != nil {
+ panic(err)
+ }
+ idpID, err := createZitadelResources(apiURL, pat, domain, metadata)
+ if err != nil {
+ panic(err)
+ }
+
+ lis := bind.Socket(":" + baseURL.Port())
+ goji.Handle("/*", idpServer)
+
+ go func() {
+ goji.ServeListener(lis)
+ }()
+
+ addService(idpServer, apiURL+"/idps/"+idpID+"/saml/metadata")
+ addUsers(idpServer)
+
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+ <-sigChan
+
+ if err := lis.Close(); err != nil {
+ log.Fatalf("HTTP shutdown error: %v", err)
+ }
+}
+
+func readPAT(path string) string {
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ pat, err := io.ReadAll(f)
+ if err != nil {
+ panic(err)
+ }
+ return strings.Trim(string(pat), "\n")
+}
+
+func addService(idpServer *samlidp.Server, spURLStr string) {
+ metadataResp, err := http.Get(spURLStr)
+ if err != nil {
+ panic(err)
+ }
+ defer metadataResp.Body.Close()
+
+ idpServer.HandlePutService(
+ web.C{URLParams: map[string]string{"id": spURLStr}},
+ httptest.NewRecorder(),
+ httptest.NewRequest(http.MethodPost, spURLStr, metadataResp.Body),
+ )
+}
+
+func getSPMetadata(r io.Reader) (spMetadata *saml.EntityDescriptor, err error) {
+ var data []byte
+ if data, err = io.ReadAll(r); err != nil {
+ return nil, err
+ }
+
+ spMetadata = &saml.EntityDescriptor{}
+ if err := xrv.Validate(bytes.NewBuffer(data)); err != nil {
+ return nil, err
+ }
+
+ if err := xml.Unmarshal(data, &spMetadata); err != nil {
+ if err.Error() == "expected element type but have " {
+ entities := &saml.EntitiesDescriptor{}
+ if err := xml.Unmarshal(data, &entities); err != nil {
+ return nil, err
+ }
+
+ for _, e := range entities.EntityDescriptors {
+ if len(e.SPSSODescriptors) > 0 {
+ return &e, nil
+ }
+ }
+
+ // there were no SPSSODescriptors in the response
+ return nil, errors.New("metadata contained no service provider metadata")
+ }
+
+ return nil, err
+ }
+
+ return spMetadata, nil
+}
+
+func addUsers(idpServer *samlidp.Server) {
+ hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("hunter2"), bcrypt.DefaultCost)
+ err := idpServer.Store.Put("/users/alice", samlidp.User{Name: "alice",
+ HashedPassword: hashedPassword,
+ Groups: []string{"Administrators", "Users"},
+ Email: "alice@example.com",
+ CommonName: "Alice Smith",
+ Surname: "Smith",
+ GivenName: "Alice",
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ err = idpServer.Store.Put("/users/bob", samlidp.User{
+ Name: "bob",
+ HashedPassword: hashedPassword,
+ Groups: []string{"Users"},
+ Email: "bob@example.com",
+ CommonName: "Bob Smith",
+ Surname: "Smith",
+ GivenName: "Bob",
+ })
+ if err != nil {
+ panic(err)
+ }
+}
+
+func createZitadelResources(apiURL, pat, domain string, metadata []byte) (string, error) {
+ idpID, err := CreateIDP(apiURL, pat, domain, metadata)
+ if err != nil {
+ return "", err
+ }
+ return idpID, ActivateIDP(apiURL, pat, domain, idpID)
+}
+
+type createIDP struct {
+ Name string `json:"name"`
+ MetadataXml string `json:"metadataXml"`
+ Binding string `json:"binding"`
+ WithSignedRequest bool `json:"withSignedRequest"`
+ ProviderOptions providerOptions `json:"providerOptions"`
+ NameIdFormat string `json:"nameIdFormat"`
+}
+type providerOptions struct {
+ IsLinkingAllowed bool `json:"isLinkingAllowed"`
+ IsCreationAllowed bool `json:"isCreationAllowed"`
+ IsAutoCreation bool `json:"isAutoCreation"`
+ IsAutoUpdate bool `json:"isAutoUpdate"`
+ AutoLinking string `json:"autoLinking"`
+}
+
+type idp struct {
+ ID string `json:"id"`
+}
+
+func CreateIDP(apiURL, pat, domain string, idpMetadata []byte) (string, error) {
+ encoded := make([]byte, base64.URLEncoding.EncodedLen(len(idpMetadata)))
+ base64.URLEncoding.Encode(encoded, idpMetadata)
+
+ createIDP := &createIDP{
+ Name: "CREWJAM",
+ MetadataXml: string(encoded),
+ Binding: "SAML_BINDING_REDIRECT",
+ WithSignedRequest: false,
+ ProviderOptions: providerOptions{
+ IsLinkingAllowed: true,
+ IsCreationAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ AutoLinking: "AUTO_LINKING_OPTION_USERNAME",
+ },
+ NameIdFormat: "SAML_NAME_ID_FORMAT_PERSISTENT",
+ }
+
+ resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/saml", pat, domain, createIDP)
+ if err != nil {
+ return "", err
+ }
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ idp := new(idp)
+ if err := json.Unmarshal(data, idp); err != nil {
+ return "", err
+ }
+ return idp.ID, nil
+}
+
+type activateIDP struct {
+ IdpId string `json:"idpId"`
+}
+
+func ActivateIDP(apiURL, pat, domain string, idpID string) error {
+ activateIDP := &activateIDP{
+ IdpId: idpID,
+ }
+ _, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP)
+ return err
+}
+
+func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
+ if err != nil {
+ return nil, err
+ }
+ values := http.Header{}
+ values.Add("Authorization", "Bearer "+pat)
+ values.Add("x-forwarded-host", domain)
+ values.Add("Content-Type", "application/json")
+ req.Header = values
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/apps/login/acceptance/oidcrp/go.mod b/apps/login/acceptance/oidcrp/go.mod
new file mode 100644
index 0000000000..f2cda3058e
--- /dev/null
+++ b/apps/login/acceptance/oidcrp/go.mod
@@ -0,0 +1,26 @@
+module github.com/zitadel/typescript/acceptance/oidc
+
+go 1.24.1
+
+require (
+ github.com/google/uuid v1.6.0
+ github.com/sirupsen/logrus v1.9.3
+ github.com/zitadel/logging v0.6.1
+ github.com/zitadel/oidc/v3 v3.36.1
+)
+
+require (
+ github.com/go-jose/go-jose/v4 v4.0.5 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
+ github.com/muhlemmer/gu v0.3.1 // indirect
+ github.com/zitadel/schema v1.3.0 // indirect
+ go.opentelemetry.io/otel v1.29.0 // indirect
+ go.opentelemetry.io/otel/metric v1.29.0 // indirect
+ go.opentelemetry.io/otel/trace v1.29.0 // indirect
+ golang.org/x/crypto v0.35.0 // indirect
+ golang.org/x/oauth2 v0.28.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+)
diff --git a/apps/login/acceptance/oidcrp/go.sum b/apps/login/acceptance/oidcrp/go.sum
new file mode 100644
index 0000000000..33244ea6eb
--- /dev/null
+++ b/apps/login/acceptance/oidcrp/go.sum
@@ -0,0 +1,67 @@
+github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
+github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
+github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
+github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
+github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
+github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
+github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
+github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
+github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
+github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
+github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0=
+github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs=
+github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
+github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
+go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/apps/login/acceptance/oidcrp/main.go b/apps/login/acceptance/oidcrp/main.go
new file mode 100644
index 0000000000..72ae5f57e9
--- /dev/null
+++ b/apps/login/acceptance/oidcrp/main.go
@@ -0,0 +1,322 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "log/slog"
+ "net/http"
+ "os"
+ "os/signal"
+ "strings"
+ "sync/atomic"
+ "syscall"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+
+ "github.com/zitadel/logging"
+ "github.com/zitadel/oidc/v3/pkg/client/rp"
+ httphelper "github.com/zitadel/oidc/v3/pkg/http"
+ "github.com/zitadel/oidc/v3/pkg/oidc"
+)
+
+var (
+ callbackPath = "/auth/callback"
+ key = []byte("test1234test1234")
+)
+
+func main() {
+ apiURL := os.Getenv("API_URL")
+ pat := readPAT(os.Getenv("PAT_FILE"))
+ domain := os.Getenv("API_DOMAIN")
+ loginURL := os.Getenv("LOGIN_URL")
+ issuer := os.Getenv("ISSUER")
+ port := os.Getenv("PORT")
+ scopeList := strings.Split(os.Getenv("SCOPES"), " ")
+
+ redirectURI := fmt.Sprintf("%s%s", issuer, callbackPath)
+ cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
+
+ clientID, clientSecret, err := createZitadelResources(apiURL, pat, domain, redirectURI, loginURL)
+ if err != nil {
+ panic(err)
+ }
+
+ logger := slog.New(
+ slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+ AddSource: true,
+ Level: slog.LevelDebug,
+ }),
+ )
+ client := &http.Client{
+ Timeout: time.Minute,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ }
+ // enable outgoing request logging
+ logging.EnableHTTPClient(client,
+ logging.WithClientGroup("client"),
+ )
+
+ options := []rp.Option{
+ rp.WithCookieHandler(cookieHandler),
+ rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
+ rp.WithHTTPClient(client),
+ rp.WithLogger(logger),
+ rp.WithSigningAlgsFromDiscovery(),
+ rp.WithCustomDiscoveryUrl(issuer + "/.well-known/openid-configuration"),
+ }
+ if clientSecret == "" {
+ options = append(options, rp.WithPKCE(cookieHandler))
+ }
+
+ // One can add a logger to the context,
+ // pre-defining log attributes as required.
+ ctx := logging.ToContext(context.TODO(), logger)
+ provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopeList, options...)
+ if err != nil {
+ logrus.Fatalf("error creating provider %s", err.Error())
+ }
+
+ // generate some state (representing the state of the user in your application,
+ // e.g. the page where he was before sending him to login
+ state := func() string {
+ return uuid.New().String()
+ }
+
+ urlOptions := []rp.URLParamOpt{
+ rp.WithPromptURLParam("Welcome back!"),
+ }
+
+ // register the AuthURLHandler at your preferred path.
+ // the AuthURLHandler creates the auth request and redirects the user to the auth server.
+ // including state handling with secure cookie and the possibility to use PKCE.
+ // Prompts can optionally be set to inform the server of
+ // any messages that need to be prompted back to the user.
+ http.Handle("/login", rp.AuthURLHandler(
+ state,
+ provider,
+ urlOptions...,
+ ))
+
+ // for demonstration purposes the returned userinfo response is written as JSON object onto response
+ marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
+ fmt.Println("access token", tokens.AccessToken)
+ fmt.Println("refresh token", tokens.RefreshToken)
+ fmt.Println("id token", tokens.IDToken)
+
+ data, err := json.Marshal(info)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("content-type", "application/json")
+ w.Write(data)
+ }
+
+ // register the CodeExchangeHandler at the callbackPath
+ // the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
+ // with the returned tokens from the token endpoint
+ // in this example the callback function itself is wrapped by the UserinfoCallback which
+ // will call the Userinfo endpoint, check the sub and pass the info into the callback function
+ http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider))
+
+ // if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for:
+ //
+ // http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
+
+ // simple counter for request IDs
+ var counter atomic.Int64
+ // enable incomming request logging
+ mw := logging.Middleware(
+ logging.WithLogger(logger),
+ logging.WithGroup("server"),
+ logging.WithIDFunc(func() slog.Attr {
+ return slog.Int64("id", counter.Add(1))
+ }),
+ )
+
+ http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return }))
+ fmt.Println("/healthy returns 200 OK")
+
+ server := &http.Server{
+ Addr: ":" + port,
+ Handler: mw(http.DefaultServeMux),
+ }
+ go func() {
+ if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+ log.Fatalf("HTTP server error: %v", err)
+ }
+ log.Println("Stopped serving new connections.")
+ }()
+
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+ <-sigChan
+
+ shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
+ defer shutdownRelease()
+
+ if err := server.Shutdown(shutdownCtx); err != nil {
+ log.Fatalf("HTTP shutdown error: %v", err)
+ }
+}
+
+func readPAT(path string) string {
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ pat, err := io.ReadAll(f)
+ if err != nil {
+ panic(err)
+ }
+ return strings.Trim(string(pat), "\n")
+}
+
+func createZitadelResources(apiURL, pat, domain, redirectURI, loginURL string) (string, string, error) {
+ projectID, err := CreateProject(apiURL, pat, domain)
+ if err != nil {
+ return "", "", err
+ }
+ return CreateApp(apiURL, pat, domain, projectID, redirectURI, loginURL)
+}
+
+type project struct {
+ ID string `json:"id"`
+}
+type createProject struct {
+ Name string `json:"name"`
+ ProjectRoleAssertion bool `json:"projectRoleAssertion"`
+ ProjectRoleCheck bool `json:"projectRoleCheck"`
+ HasProjectCheck bool `json:"hasProjectCheck"`
+ PrivateLabelingSetting string `json:"privateLabelingSetting"`
+}
+
+func CreateProject(apiURL, pat, domain string) (string, error) {
+ createProject := &createProject{
+ Name: "OIDC",
+ ProjectRoleAssertion: false,
+ ProjectRoleCheck: false,
+ HasProjectCheck: false,
+ PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED",
+ }
+ resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject)
+ if err != nil {
+ return "", err
+ }
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ p := new(project)
+ if err := json.Unmarshal(data, p); err != nil {
+ return "", err
+ }
+ fmt.Printf("projectID: %+v\n", p.ID)
+ return p.ID, nil
+}
+
+type createApp struct {
+ Name string `json:"name"`
+ RedirectUris []string `json:"redirectUris"`
+ ResponseTypes []string `json:"responseTypes"`
+ GrantTypes []string `json:"grantTypes"`
+ AppType string `json:"appType"`
+ AuthMethodType string `json:"authMethodType"`
+ PostLogoutRedirectUris []string `json:"postLogoutRedirectUris"`
+ Version string `json:"version"`
+ DevMode bool `json:"devMode"`
+ AccessTokenType string `json:"accessTokenType"`
+ AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion"`
+ IdTokenRoleAssertion bool `json:"idTokenRoleAssertion"`
+ IdTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion"`
+ ClockSkew string `json:"clockSkew"`
+ AdditionalOrigins []string `json:"additionalOrigins"`
+ SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage"`
+ BackChannelLogoutUri []string `json:"backChannelLogoutUri"`
+ LoginVersion version `json:"loginVersion"`
+}
+
+type version struct {
+ LoginV2 loginV2 `json:"loginV2"`
+}
+type loginV2 struct {
+ BaseUri string `json:"baseUri"`
+}
+
+type app struct {
+ ClientID string `json:"clientId"`
+ ClientSecret string `json:"clientSecret"`
+}
+
+func CreateApp(apiURL, pat, domain, projectID string, redirectURI, loginURL string) (string, string, error) {
+ createApp := &createApp{
+ Name: "OIDC",
+ RedirectUris: []string{redirectURI},
+ ResponseTypes: []string{"OIDC_RESPONSE_TYPE_CODE"},
+ GrantTypes: []string{"OIDC_GRANT_TYPE_AUTHORIZATION_CODE"},
+ AppType: "OIDC_APP_TYPE_WEB",
+ AuthMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC",
+ Version: "OIDC_VERSION_1_0",
+ DevMode: true,
+ AccessTokenType: "OIDC_TOKEN_TYPE_BEARER",
+ AccessTokenRoleAssertion: true,
+ IdTokenRoleAssertion: true,
+ IdTokenUserinfoAssertion: true,
+ ClockSkew: "1s",
+ SkipNativeAppSuccessPage: true,
+ LoginVersion: version{
+ LoginV2: loginV2{
+ BaseUri: loginURL,
+ },
+ },
+ }
+
+ resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/oidc", pat, domain, createApp)
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", "", err
+ }
+ defer resp.Body.Close()
+
+ a := new(app)
+ if err := json.Unmarshal(data, a); err != nil {
+ return "", "", err
+ }
+ return a.ClientID, a.ClientSecret, err
+}
+
+func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
+ if err != nil {
+ return nil, err
+ }
+ values := http.Header{}
+ values.Add("Authorization", "Bearer "+pat)
+ values.Add("x-forwarded-host", domain)
+ values.Add("Content-Type", "application/json")
+ req.Header = values
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/apps/login/acceptance/package.json b/apps/login/acceptance/package.json
new file mode 100644
index 0000000000..fc4a191373
--- /dev/null
+++ b/apps/login/acceptance/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "login-test-acceptance",
+ "private": true,
+ "scripts": {
+ "test:acceptance": "dotenv -e ../login/.env.test.local playwright",
+ "test:acceptance:setup": "cd ../.. && make login_test_acceptance_setup_env && NODE_ENV=test turbo run test:acceptance:setup:dev",
+ "test:acceptance:setup:dev": "cd ../.. && make login_test_acceptance_setup_dev"
+ },
+ "devDependencies": {
+ "@faker-js/faker": "^9.7.0",
+ "@otplib/core": "^12.0.0",
+ "@otplib/plugin-crypto": "^12.0.0",
+ "@otplib/plugin-thirty-two": "^12.0.0",
+ "@playwright/test": "^1.52.0",
+ "dotenv-cli": "^8.0.0",
+ "gaxios": "^7.1.0",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/apps/login/acceptance/pat/.gitignore b/apps/login/acceptance/pat/.gitignore
new file mode 100644
index 0000000000..bf27f3114d
--- /dev/null
+++ b/apps/login/acceptance/pat/.gitignore
@@ -0,0 +1,3 @@
+*
+!.gitignore
+!.gitkeep
diff --git a/console/src/assets/.gitkeep b/apps/login/acceptance/pat/.gitkeep
similarity index 100%
rename from console/src/assets/.gitkeep
rename to apps/login/acceptance/pat/.gitkeep
diff --git a/apps/login/acceptance/playwright-report/.gitignore b/apps/login/acceptance/playwright-report/.gitignore
new file mode 100644
index 0000000000..bf27f3114d
--- /dev/null
+++ b/apps/login/acceptance/playwright-report/.gitignore
@@ -0,0 +1,3 @@
+*
+!.gitignore
+!.gitkeep
diff --git a/build/zitadel/generate-grpc.sh b/apps/login/acceptance/playwright-report/.gitkeep
old mode 100755
new mode 100644
similarity index 100%
rename from build/zitadel/generate-grpc.sh
rename to apps/login/acceptance/playwright-report/.gitkeep
diff --git a/apps/login/acceptance/playwright.config.ts b/apps/login/acceptance/playwright.config.ts
new file mode 100644
index 0000000000..8025db3238
--- /dev/null
+++ b/apps/login/acceptance/playwright.config.ts
@@ -0,0 +1,78 @@
+import { defineConfig, devices } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+
+dotenv.config({ path: path.resolve(__dirname, "../login/.env.test.local") });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: "./tests",
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ expect: {
+ timeout: 10_000, // 10 seconds
+ },
+ timeout: 300 * 1000, // 5 minutes
+ globalTimeout: 30 * 60_000, // 30 minutes
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: [
+ ["line"],
+ ["html", { open: process.env.CI ? "never" : "on-failure", host: "0.0.0.0", outputFolder: "./playwright-report/html" }],
+ ],
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: process.env.LOGIN_BASE_URL || "http://127.0.0.1:3000",
+ trace: "retain-on-failure",
+ headless: true,
+ screenshot: "only-on-failure",
+ video: "retain-on-failure",
+ ignoreHTTPSErrors: true,
+ },
+ outputDir: "test-results/results",
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+ /*
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
+ },
+ TODO: webkit fails. Is this a bug?
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+ */
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+});
diff --git a/apps/login/acceptance/samlsp/go.mod b/apps/login/acceptance/samlsp/go.mod
new file mode 100644
index 0000000000..9986149bfb
--- /dev/null
+++ b/apps/login/acceptance/samlsp/go.mod
@@ -0,0 +1,18 @@
+module github.com/zitadel/typescript/acceptance/saml
+
+go 1.24.0
+
+require github.com/crewjam/saml v0.4.14
+
+require (
+ github.com/beevik/etree v1.5.0 // indirect
+ github.com/crewjam/httperr v0.2.0 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/jonboulle/clockwork v0.5.0 // indirect
+ github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/russellhaering/goxmldsig v1.5.0 // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
+ golang.org/x/crypto v0.36.0 // indirect
+)
diff --git a/apps/login/acceptance/samlsp/go.sum b/apps/login/acceptance/samlsp/go.sum
new file mode 100644
index 0000000000..3394a39410
--- /dev/null
+++ b/apps/login/acceptance/samlsp/go.sum
@@ -0,0 +1,38 @@
+github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
+github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
+github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
+github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
+github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
+github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
+github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
+github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
+github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
+github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/apps/login/acceptance/samlsp/main.go b/apps/login/acceptance/samlsp/main.go
new file mode 100644
index 0000000000..9dcfd13796
--- /dev/null
+++ b/apps/login/acceptance/samlsp/main.go
@@ -0,0 +1,271 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/crewjam/saml/samlsp"
+)
+
+var keyPair = func() tls.Certificate {
+ cert := []byte(`-----BEGIN CERTIFICATE-----
+MIIDITCCAgmgAwIBAgIUKjAUmxsHO44X+/TKBNciPgNl1GEwDQYJKoZIhvcNAQEL
+BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIxOTEz
+Mzc1MVoXDTI1MTIxOTEzMzc1MVowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0QYuJsayILRI
+hVT7G1DlitVSXnt1iw3gEXJZfe81Egz06fUbvXF6Yo1LJmwYpqe/rm+hf4FNUb8e
+2O+LH2FieA9FkVe4P2gKOzw87A/KxvpV8stgNgl4LlqRCokbc1AzeE/NiLr5TcTD
+RXm3DUcYxXxinprtDu2jftFysaOZmNAukvE/iL6qS3X6ggVEDDM7tY9n5FV2eJ4E
+p0ImKfypi2aZYROxOK+v5x9ryFRMl4y07lMDvmtcV45uXYmfGNCgG9PNf91Kk/mh
+JxEQbxycJwFoSi9XWljR8ahPdO11LXG7Dsj/RVbY8k2LdKNstl6Ae3aCpbe9u2Pj
+vxYs1bVJuQIDAQABo1MwUTAdBgNVHQ4EFgQU+mRVN5HYJWgnpopReaLhf2cMcoYw
+HwYDVR0jBBgwFoAU+mRVN5HYJWgnpopReaLhf2cMcoYwDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAQEABJpHVuc9tGhD04infRVlofvqXIUizTlOrjZX
+vozW9pIhSWEHX8o+sJP8AMZLnrsdq+bm0HE0HvgYrw7Lb8pd4FpR46TkFHjeukoj
+izqfgckjIBl2nwPGlynbKA0/U/rTCSxVt7XiAn+lgYUGIpOzNdk06/hRMitrMNB7
+t2C97NseVC4b1ZgyFrozsefCfUmD8IJF0+XJ4Wzmsh0jRrI8koCtVmPYnKn6vw1b
+cZprg/97CWHYrsavd406wOB60CMtYl83Q16ucOF1dretDFqJC5kY+aFLvuqfag2+
+kIaoPV1MnGsxveQyyHdOsEatS5XOv/1OWcmnvePDPxcvb9jCcw==
+-----END CERTIFICATE-----
+`)
+ key := []byte(`-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRBi4mxrIgtEiF
+VPsbUOWK1VJee3WLDeARcll97zUSDPTp9Ru9cXpijUsmbBimp7+ub6F/gU1Rvx7Y
+74sfYWJ4D0WRV7g/aAo7PDzsD8rG+lXyy2A2CXguWpEKiRtzUDN4T82IuvlNxMNF
+ebcNRxjFfGKemu0O7aN+0XKxo5mY0C6S8T+IvqpLdfqCBUQMMzu1j2fkVXZ4ngSn
+QiYp/KmLZplhE7E4r6/nH2vIVEyXjLTuUwO+a1xXjm5diZ8Y0KAb081/3UqT+aEn
+ERBvHJwnAWhKL1daWNHxqE907XUtcbsOyP9FVtjyTYt0o2y2XoB7doKlt727Y+O/
+FizVtUm5AgMBAAECggEACak+l5f6Onj+u5vrjc4JyAaXW6ra6loSM9g8Uu3sHukW
+plwoA7Pzp0u20CAxrP1Gpqw984/hSCCcb0Q2ItWMWLaC/YZni5W2WFnOyo3pzlPa
+hmH4UNMT+ReCSfF/oW8w69QLcNEMjhfEu0i2iWBygIlA4SoRwC2Db6yEX7nLMwUB
+6AICid9hfeACNRz/nq5ytdcHdmcB7Ptgb9jLiXr6RZw26g5AsRPHU3LdcyZAOXjP
+aUHriHuHQFKAVkoEUxslvCB6ePCTCpB0bSAuzQbeGoY8fmvmNSCvJ1vrH5hiSUYp
+Axtl5iNgFl5o9obb0eBYlY9x3pMSz0twdbCwfR7HAQKBgQDtWhmFm0NaJALoY+tq
+lIIC0EOMSrcRIlgeXr6+g8womuDOMi5m/Nr5Mqt4mPOdP4HytrQb+a/ZmEm17KHh
+mQb1vwH8ffirCBHbPNC1vwSNoxDKv9E6OysWlKiOzxPFSVZr3dKl2EMX6qi17n0l
+LBrGXXaNPgYiHSmwBA5CZvvouQKBgQDhclGJfZfuoubQkUuz8yOA2uxalh/iUmQ/
+G8ac6/w7dmnL9pXehqCWh06SeC3ZvW7yrf7IIGx4sTJji2FzQ+8Ta6pPELMyBEXr
+1VirIFrlNVMlMQEbZcbzdzEhchM1RUpZJtl3b4amvH21UcRB69d9klcDRisKoFRm
+k0P9QLHpAQKBgQDh5J9nphZa4u0ViYtTW1XFIbs3+R/0IbCl7tww67TRbF3KQL4i
+7EHna88ALumkXf3qJvKRsXgoaqS0jSqgUAjst8ZHLQkOldaQxneIkezedDSWEisp
+9YgTrJYjnHefiyXB8VL63jE0wPOiewEF8Mzmv6sFz+L8cq7rQ2Di16qmmQKBgQDH
+bvCwVxkrMpJK2O2GH8U9fOzu6bUE6eviY/jb4mp8U7EdjGJhuuieoM2iBoxQ/SID
+rmYftYcfcWlo4+juJZ99p5W+YcCTs3IDQPUyVOnzr6uA0Avxp6RKxhsBQj+5tTUj
+Dpn77P3JzB7MYqvhwPcdD3LH46+5s8FWCFpx02RPAQKBgARbngtggfifatcsMC7n
+lSv/FVLH7LYQAHdoW/EH5Be7FeeP+eQvGXwh1dgl+u0VZO8FvI8RwFganpBRR2Nc
+ZSBRIb0fSUlTvIsckSWjpEvUJUomJXyi4PIZAfNvd9/u1uLInQiCDtObwb6hnLTU
+FHHEZ+dR4eMaJp6PhNm8hu2O
+-----END PRIVATE KEY-----
+`)
+
+ kp, err := tls.X509KeyPair(cert, key)
+ if err != nil {
+ panic(err)
+ }
+ kp.Leaf, err = x509.ParseCertificate(kp.Certificate[0])
+ if err != nil {
+ panic(err)
+ }
+ return kp
+}()
+
+func hello(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "UserName"))
+}
+
+func main() {
+ apiURL := os.Getenv("API_URL")
+ pat := readPAT(os.Getenv("PAT_FILE"))
+ domain := os.Getenv("API_DOMAIN")
+ loginURL := os.Getenv("LOGIN_URL")
+ idpURL := os.Getenv("IDP_URL")
+ host := os.Getenv("HOST")
+ port := os.Getenv("PORT")
+
+ idpMetadataURL, err := url.Parse(idpURL)
+ if err != nil {
+ panic(err)
+ }
+ idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient,
+ *idpMetadataURL)
+ if err != nil {
+ panic(fmt.Errorf("failed to fetch IDP metadata from %s: %w", idpURL, err))
+ }
+ fmt.Printf("idpMetadata: %+v\n", idpMetadata)
+ rootURL, err := url.Parse(host + ":" + port)
+ if err != nil {
+ panic(err)
+ }
+
+ samlSP, err := samlsp.New(samlsp.Options{
+ URL: *rootURL,
+ Key: keyPair.PrivateKey.(*rsa.PrivateKey),
+ Certificate: keyPair.Leaf,
+ IDPMetadata: idpMetadata,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ server := &http.Server{
+ Addr: ":" + port,
+ }
+ app := http.HandlerFunc(hello)
+ http.Handle("/hello", samlSP.RequireAccount(app))
+ http.Handle("/saml/", samlSP)
+ go func() {
+ if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+ log.Fatalf("HTTP server error: %v", err)
+ }
+ log.Println("Stopped serving new connections.")
+ }()
+
+ metadata, err := xml.MarshalIndent(samlSP.ServiceProvider.Metadata(), "", " ")
+ if err != nil {
+ panic(err)
+ }
+ if err := createZitadelResources(apiURL, pat, domain, metadata, loginURL); err != nil {
+ panic(err)
+ }
+
+ http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return }))
+ fmt.Println("/healthy returns 200 OK")
+
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+ <-sigChan
+
+ shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
+ defer shutdownRelease()
+
+ if err := server.Shutdown(shutdownCtx); err != nil {
+ log.Fatalf("HTTP shutdown error: %v", err)
+ }
+}
+
+func readPAT(path string) string {
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ pat, err := io.ReadAll(f)
+ if err != nil {
+ panic(err)
+ }
+ return strings.Trim(string(pat), "\n")
+}
+
+func createZitadelResources(apiURL, pat, domain string, metadata []byte, loginURL string) error {
+ projectID, err := CreateProject(apiURL, pat, domain)
+ if err != nil {
+ return err
+ }
+ return CreateApp(apiURL, pat, domain, projectID, metadata, loginURL)
+}
+
+type project struct {
+ ID string `json:"id"`
+}
+type createProject struct {
+ Name string `json:"name"`
+ ProjectRoleAssertion bool `json:"projectRoleAssertion"`
+ ProjectRoleCheck bool `json:"projectRoleCheck"`
+ HasProjectCheck bool `json:"hasProjectCheck"`
+ PrivateLabelingSetting string `json:"privateLabelingSetting"`
+}
+
+func CreateProject(apiURL, pat, domain string) (string, error) {
+ createProject := &createProject{
+ Name: "SAML",
+ ProjectRoleAssertion: false,
+ ProjectRoleCheck: false,
+ HasProjectCheck: false,
+ PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED",
+ }
+ resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject)
+ if err != nil {
+ return "", err
+ }
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ p := new(project)
+ if err := json.Unmarshal(data, p); err != nil {
+ return "", err
+ }
+ return p.ID, nil
+}
+
+type createApp struct {
+ Name string `json:"name"`
+ MetadataXml string `json:"metadataXml"`
+ LoginVersion version `json:"loginVersion"`
+}
+type version struct {
+ LoginV2 loginV2 `json:"loginV2"`
+}
+type loginV2 struct {
+ BaseUri string `json:"baseUri"`
+}
+
+func CreateApp(apiURL, pat, domain, projectID string, spMetadata []byte, loginURL string) error {
+ encoded := make([]byte, base64.URLEncoding.EncodedLen(len(spMetadata)))
+ base64.URLEncoding.Encode(encoded, spMetadata)
+
+ createApp := &createApp{
+ Name: "SAML",
+ MetadataXml: string(encoded),
+ LoginVersion: version{
+ LoginV2: loginV2{
+ BaseUri: loginURL,
+ },
+ },
+ }
+ _, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/saml", pat, domain, createApp)
+ if err != nil {
+ return fmt.Errorf("error creating saml app with request %+v: %v", *createApp, err)
+ }
+ return err
+}
+
+func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
+ if err != nil {
+ return nil, err
+ }
+ values := http.Header{}
+ values.Add("Authorization", "Bearer "+pat)
+ values.Add("x-forwarded-host", domain)
+ values.Add("Content-Type", "application/json")
+ req.Header = values
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
diff --git a/apps/login/acceptance/setup/go.mod b/apps/login/acceptance/setup/go.mod
new file mode 100644
index 0000000000..7be166ef9b
--- /dev/null
+++ b/apps/login/acceptance/setup/go.mod
@@ -0,0 +1,3 @@
+module github.com/zitadel/typescript/apps/login-test-acceptance/setup
+
+go 1.23.3
diff --git a/apps/login/acceptance/setup/go.sum b/apps/login/acceptance/setup/go.sum
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/login/acceptance/setup/main.go b/apps/login/acceptance/setup/main.go
new file mode 100644
index 0000000000..38dd16da61
--- /dev/null
+++ b/apps/login/acceptance/setup/main.go
@@ -0,0 +1,3 @@
+package main
+
+func main() {}
diff --git a/apps/login/acceptance/setup/setup.sh b/apps/login/acceptance/setup/setup.sh
new file mode 100755
index 0000000000..9d1a04e18f
--- /dev/null
+++ b/apps/login/acceptance/setup/setup.sh
@@ -0,0 +1,139 @@
+#!/bin/sh
+
+set -e pipefail
+
+PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat}
+LOGIN_BASE_URL=${LOGIN_BASE_URL:-"http://localhost:3000"}
+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"}"
+WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.test.local}
+
+if [ -z "${PAT}" ]; then
+ echo "Reading PAT from file ${PAT_FILE}"
+ PAT=$(cat ${PAT_FILE})
+fi
+
+#################################################################
+# ServiceAccount as Login Client
+#################################################################
+
+SERVICEACCOUNT_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/machine" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json" \
+ -d "{\"userName\": \"login\", \"name\": \"Login v2\", \"description\": \"Serviceaccount for Login v2\", \"accessTokenType\": \"ACCESS_TOKEN_TYPE_BEARER\"}")
+echo "Received ServiceAccount response: ${SERVICEACCOUNT_RESPONSE}"
+
+SERVICEACCOUNT_ID=$(echo ${SERVICEACCOUNT_RESPONSE} | jq -r '. | .userId')
+echo "Received ServiceAccount ID: ${SERVICEACCOUNT_ID}"
+
+MEMBER_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/members" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json" \
+ -d "{\"userId\": \"${SERVICEACCOUNT_ID}\", \"roles\": [\"IAM_LOGIN_CLIENT\"]}")
+echo "Received Member response: ${MEMBER_RESPONSE}"
+
+SA_PAT_RESPONSE=$(curl -s --request POST \
+ --url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/${SERVICEACCOUNT_ID}/pats" \
+ --header "Authorization: Bearer ${PAT}" \
+ --header "Host: ${ZITADEL_API_DOMAIN}" \
+ --header "Content-Type: application/json" \
+ -d "{\"expirationDate\": \"2519-04-01T08:45:00.000000Z\"}")
+echo "Received Member response: ${MEMBER_RESPONSE}"
+
+SA_PAT=$(echo ${SA_PAT_RESPONSE} | jq -r '. | .token')
+echo "Received ServiceAccount Token: ${SA_PAT}"
+
+#################################################################
+# Environment files
+#################################################################
+
+echo "Writing environment file ${WRITE_ENVIRONMENT_FILE}."
+
+echo "ZITADEL_API_URL=${ZITADEL_API_URL}
+ZITADEL_SERVICE_USER_TOKEN=${SA_PAT}
+ZITADEL_ADMIN_TOKEN=${PAT}
+SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
+EMAIL_VERIFICATION=true
+DEBUG=false
+LOGIN_BASE_URL=${LOGIN_BASE_URL}
+NODE_TLS_REJECT_UNAUTHORIZED=0
+ZITADEL_ADMIN_USER=${ZITADEL_ADMIN_USER:-"zitadel-admin@zitadel.localhost"}
+NEXT_PUBLIC_BASE_PATH=/ui/v2/login
+" > ${WRITE_ENVIRONMENT_FILE}
+
+echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
+cat ${WRITE_ENVIRONMENT_FILE}
+
+#################################################################
+# SMS provider with HTTP
+#################################################################
+
+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}"
+
+SMSHTTP_ID=$(echo ${SMSHTTP_RESPONSE} | jq -r '. | .id')
+echo "Received SMS HTTP ID: ${SMSHTTP_ID}"
+
+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}"
+
+#################################################################
+# Email provider with HTTP
+#################################################################
+
+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}"
+
+EMAILHTTP_ID=$(echo ${EMAILHTTP_RESPONSE} | jq -r '. | .id')
+echo "Received Email HTTP ID: ${EMAILHTTP_ID}"
+
+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
+
diff --git a/apps/login/acceptance/sink/go.mod b/apps/login/acceptance/sink/go.mod
new file mode 100644
index 0000000000..1da7622b58
--- /dev/null
+++ b/apps/login/acceptance/sink/go.mod
@@ -0,0 +1,3 @@
+module github.com/zitadel/typescript/acceptance/sink
+
+go 1.24.0
diff --git a/apps/login/acceptance/sink/go.sum b/apps/login/acceptance/sink/go.sum
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/login/acceptance/sink/main.go b/apps/login/acceptance/sink/main.go
new file mode 100644
index 0000000000..f3795ba0d0
--- /dev/null
+++ b/apps/login/acceptance/sink/main.go
@@ -0,0 +1,111 @@
+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
+ }
+ msg, ok := messages[response.Recipient]
+ if !ok {
+ http.Error(w, "No messages found for recipient: "+response.Recipient, http.StatusNotFound)
+ return
+ }
+ serializableData, err := json.Marshal(msg)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ 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")
+ http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return }))
+ fmt.Println("/healthy returns 200 OK")
+ err := http.ListenAndServe(":"+*port, nil)
+ if err != nil {
+ panic("Server could not be started: " + err.Error())
+ }
+}
diff --git a/apps/login/acceptance/test-results/.gitignore b/apps/login/acceptance/test-results/.gitignore
new file mode 100644
index 0000000000..bf27f3114d
--- /dev/null
+++ b/apps/login/acceptance/test-results/.gitignore
@@ -0,0 +1,3 @@
+*
+!.gitignore
+!.gitkeep
diff --git a/apps/login/acceptance/test-results/.gitkeep b/apps/login/acceptance/test-results/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/login/acceptance/tests/admin.spec.ts b/apps/login/acceptance/tests/admin.spec.ts
new file mode 100644
index 0000000000..13b748fc63
--- /dev/null
+++ b/apps/login/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, process.env["ZITADEL_ADMIN_USER"], "Password1!");
+ await loginScreenExpect(page, "ZITADEL Admin");
+});
diff --git a/apps/login/acceptance/tests/code-screen.ts b/apps/login/acceptance/tests/code-screen.ts
new file mode 100644
index 0000000000..3ab9dad26d
--- /dev/null
+++ b/apps/login/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/apps/login/acceptance/tests/code.ts b/apps/login/acceptance/tests/code.ts
new file mode 100644
index 0000000000..e27d1f6150
--- /dev/null
+++ b/apps/login/acceptance/tests/code.ts
@@ -0,0 +1,17 @@
+import { Page } from "@playwright/test";
+import { codeScreen } from "./code-screen";
+import { getOtpFromSink } from "./sink";
+
+export async function otpFromSink(page: Page, key: string) {
+ const c = await getOtpFromSink(key);
+ await code(page, c);
+}
+
+export async function code(page: Page, code: string) {
+ await codeScreen(page, code);
+ await page.getByTestId("submit-button").click();
+}
+
+export async function codeResend(page: Page) {
+ await page.getByTestId("resend-button").click();
+}
diff --git a/apps/login/acceptance/tests/email-verify-screen.ts b/apps/login/acceptance/tests/email-verify-screen.ts
new file mode 100644
index 0000000000..b077ecb424
--- /dev/null
+++ b/apps/login/acceptance/tests/email-verify-screen.ts
@@ -0,0 +1,12 @@
+import { expect, Page } from "@playwright/test";
+
+const codeTextInput = "code-text-input";
+
+export async function emailVerifyScreen(page: Page, code: string) {
+ await page.getByTestId(codeTextInput).pressSequentially(code);
+}
+
+export async function emailVerifyScreenExpect(page: Page, code: string) {
+ await expect(page.getByTestId(codeTextInput)).toHaveValue(code);
+ await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify email");
+}
diff --git a/apps/login/acceptance/tests/email-verify.spec.ts b/apps/login/acceptance/tests/email-verify.spec.ts
new file mode 100644
index 0000000000..2c546b8eee
--- /dev/null
+++ b/apps/login/acceptance/tests/email-verify.spec.ts
@@ -0,0 +1,69 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { emailVerify, emailVerifyResend } from "./email-verify";
+import { emailVerifyScreenExpect } from "./email-verify-screen";
+import { loginScreenExpect, loginWithPassword } from "./login";
+import { getCodeFromSink } from "./sink";
+import { PasswordUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ isEmailVerified: false,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("user email not verified, verify", async ({ user, page }) => {
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ const c = await getCodeFromSink(user.getUsername());
+ await emailVerify(page, c);
+ // wait for resend of the code
+ await page.waitForTimeout(2000);
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("user email not verified, resend, verify", async ({ user, page }) => {
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ // auto-redirect on /verify
+ await emailVerifyResend(page);
+ const c = await getCodeFromSink(user.getUsername());
+ // wait for resend of the code
+ await page.waitForTimeout(2000);
+ await emailVerify(page, c);
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("user email not verified, resend, old code", async ({ user, page }) => {
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ const c = await getCodeFromSink(user.getUsername());
+ await emailVerifyResend(page);
+ // wait for resend of the code
+ await page.waitForTimeout(2000);
+ await emailVerify(page, c);
+ await emailVerifyScreenExpect(page, c);
+});
+
+test("user email not verified, wrong code", async ({ user, page }) => {
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ // auto-redirect on /verify
+ const code = "wrong";
+ await emailVerify(page, code);
+ await emailVerifyScreenExpect(page, code);
+});
diff --git a/apps/login/acceptance/tests/email-verify.ts b/apps/login/acceptance/tests/email-verify.ts
new file mode 100644
index 0000000000..5275e82bfe
--- /dev/null
+++ b/apps/login/acceptance/tests/email-verify.ts
@@ -0,0 +1,15 @@
+import { Page } from "@playwright/test";
+import { emailVerifyScreen } from "./email-verify-screen";
+
+export async function startEmailVerify(page: Page, loginname: string) {
+ await page.goto("./verify");
+}
+
+export async function emailVerify(page: Page, code: string) {
+ await emailVerifyScreen(page, code);
+ await page.getByTestId("submit-button").click();
+}
+
+export async function emailVerifyResend(page: Page) {
+ await page.getByTestId("resend-button").click();
+}
diff --git a/apps/login/acceptance/tests/idp-apple.spec.ts b/apps/login/acceptance/tests/idp-apple.spec.ts
new file mode 100644
index 0000000000..32d3adba6b
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-apple.spec.ts
@@ -0,0 +1,102 @@
+// Note for all tests, in case Apple doesn't deliver all relevant information per default
+// We should add an action in the needed cases
+
+import test from "@playwright/test";
+
+test("login with Apple IDP", async ({ page }) => {
+ test.skip();
+ // Given an Apple IDP is configured on the organization
+ // Given the user has an Apple added as auth method
+ // User authenticates with Apple
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with Apple IDP - error", async ({ page }) => {
+ test.skip();
+ // Given an Apple IDP is configured on the organization
+ // Given the user has an Apple added as auth method
+ // User is redirected to Apple
+ // User authenticates with Apple and gets an error
+ // User is redirect back to login
+ // An error is shown to the user "Something went wrong in Apple Login"
+});
+
+test("login with Apple IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Apple is configure on the organization as only authencation method
+ // Given idp Apple is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Apple
+ // User authenticates in Apple
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Apple IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Apple is configure on the organization as only authencation method
+ // Given idp Apple is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Apple
+ // User authenticates in Apple
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Apple IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Apple is configure on the organization as only authencation method
+ // Given idp Apple is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Apple
+ // User authenticates in Apple
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Apple IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Apple is configure on the organization as only authencation method
+ // Given idp Apple is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Apple
+ // User authenticates in Apple with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Apple IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Apple is configure on the organization as only authencation method
+ // Given idp Apple is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Apple
+ // User authenticates in Apple with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Apple IDP, no user linked, user link successful", async ({ page }) => {
+ test.skip();
+ // Given idp Apple is configure on the organization as only authencation method
+ // Given idp Apple is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Apple
+ // User authenticates in Apple with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-generic-jwt.spec.ts b/apps/login/acceptance/tests/idp-generic-jwt.spec.ts
new file mode 100644
index 0000000000..d68475a226
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-generic-jwt.spec.ts
@@ -0,0 +1,99 @@
+import test from "@playwright/test";
+
+test("login with Generic JWT IDP", async ({ page }) => {
+ test.skip();
+ // Given a Generic JWT IDP is configured on the organization
+ // Given the user has Generic JWT IDP added as auth method
+ // User authenticates with the Generic JWT IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with Generic JWT IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the Generic JWT IDP is configured on the organization
+ // Given the user has Generic JWT IDP added as auth method
+ // User is redirected to the Generic JWT IDP
+ // User authenticates with the Generic JWT IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Generic JWT IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Generic JWT is configure on the organization as only authencation method
+ // Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic JWT
+ // User authenticates in Generic JWT
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic JWT IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Generic JWT is configure on the organization as only authencation method
+ // Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic JWT
+ // User authenticates in Generic JWT
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic JWT IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Generic JWT is configure on the organization as only authencation method
+ // Given idp Generic JWT is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic JWT
+ // User authenticates in Generic JWT
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Generic JWT IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Generic JWT is configure on the organization as only authencation method
+ // Given idp Generic JWT is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Generic JWT
+ // User authenticates in Generic JWT with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic JWT IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Generic JWT is configure on the organization as only authencation method
+ // Given idp Generic JWT is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Generic JWT
+ // User authenticates in Generic JWT with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Generic JWT IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Generic JWT is configure on the organization as only authencation method
+ // Given idp Generic JWT is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Generic JWT
+ // User authenticates in Generic JWT with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-generic-oauth.spec.ts b/apps/login/acceptance/tests/idp-generic-oauth.spec.ts
new file mode 100644
index 0000000000..24c25d0005
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-generic-oauth.spec.ts
@@ -0,0 +1,99 @@
+import test from "@playwright/test";
+
+test("login with Generic OAuth IDP", async ({ page }) => {
+ test.skip();
+ // Given a Generic OAuth IDP is configured on the organization
+ // Given the user has Generic OAuth IDP added as auth method
+ // User authenticates with the Generic OAuth IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with Generic OAuth IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the Generic OAuth IDP is configured on the organization
+ // Given the user has Generic OAuth IDP added as auth method
+ // User is redirected to the Generic OAuth IDP
+ // User authenticates with the Generic OAuth IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Generic OAuth IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OAuth is configure on the organization as only authencation method
+ // Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic OAuth
+ // User authenticates in Generic OAuth
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic OAuth IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OAuth is configure on the organization as only authencation method
+ // Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic OAuth
+ // User authenticates in Generic OAuth
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic OAuth IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Generic OAuth is configure on the organization as only authencation method
+ // Given idp Generic OAuth is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic OAuth
+ // User authenticates in Generic OAuth
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Generic OAuth IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OAuth is configure on the organization as only authencation method
+ // Given idp Generic OAuth is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Generic OAuth
+ // User authenticates in Generic OAuth with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic OAuth IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OAuth is configure on the organization as only authencation method
+ // Given idp Generic OAuth is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Generic OAuth
+ // User authenticates in Generic OAuth with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Generic OAuth IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OAuth is configure on the organization as only authencation method
+ // Given idp Generic OAuth is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Generic OAuth
+ // User authenticates in Generic OAuth with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-generic-oidc.spec.ts b/apps/login/acceptance/tests/idp-generic-oidc.spec.ts
new file mode 100644
index 0000000000..391481f99d
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-generic-oidc.spec.ts
@@ -0,0 +1,101 @@
+// Note, we should use a provider such as Google to test this, where we know OIDC standard is properly implemented
+
+import test from "@playwright/test";
+
+test("login with Generic OIDC IDP", async ({ page }) => {
+ test.skip();
+ // Given a Generic OIDC IDP is configured on the organization
+ // Given the user has Generic OIDC IDP added as auth method
+ // User authenticates with the Generic OIDC IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with Generic OIDC IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the Generic OIDC IDP is configured on the organization
+ // Given the user has Generic OIDC IDP added as auth method
+ // User is redirected to the Generic OIDC IDP
+ // User authenticates with the Generic OIDC IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Generic OIDC IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OIDC is configure on the organization as only authencation method
+ // Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic OIDC
+ // User authenticates in Generic OIDC
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic OIDC IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OIDC is configure on the organization as only authencation method
+ // Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic OIDC
+ // User authenticates in Generic OIDC
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic OIDC IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Generic OIDC is configure on the organization as only authencation method
+ // Given idp Generic OIDC is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Generic OIDC
+ // User authenticates in Generic OIDC
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Generic OIDC IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OIDC is configure on the organization as only authencation method
+ // Given idp Generic OIDC is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Generic OIDC
+ // User authenticates in Generic OIDC with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Generic OIDC IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OIDC is configure on the organization as only authencation method
+ // Given idp Generic OIDC is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Generic OIDC
+ // User authenticates in Generic OIDC with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Generic OIDC IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Generic OIDC is configure on the organization as only authencation method
+ // Given idp Generic OIDC is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Generic OIDC
+ // User authenticates in Generic OIDC with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-github-enterprise.spec.ts b/apps/login/acceptance/tests/idp-github-enterprise.spec.ts
new file mode 100644
index 0000000000..2c39092851
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-github-enterprise.spec.ts
@@ -0,0 +1,103 @@
+import test from "@playwright/test";
+
+test("login with GitHub Enterprise IDP", async ({ page }) => {
+ test.skip();
+ // Given a GitHub Enterprise IDP is configured on the organization
+ // Given the user has GitHub Enterprise IDP added as auth method
+ // User authenticates with the GitHub Enterprise IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with GitHub Enterprise IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the GitHub Enterprise IDP is configured on the organization
+ // Given the user has GitHub Enterprise IDP added as auth method
+ // User is redirected to the GitHub Enterprise IDP
+ // User authenticates with the GitHub Enterprise IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with GitHub Enterprise IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub Enterprise is configure on the organization as only authencation method
+ // Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given no user exists yet
+ // User is automatically redirected to GitHub Enterprise
+ // User authenticates in GitHub Enterprise
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with GitHub Enterprise IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub Enterprise is configure on the organization as only authencation method
+ // Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to GitHub Enterprise
+ // User authenticates in GitHub Enterprise
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with GitHub Enterprise IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp GitHub Enterprise is configure on the organization as only authencation method
+ // Given idp GitHub Enterprise is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to GitHub Enterprise
+ // User authenticates in GitHub Enterprise
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with GitHub Enterprise IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub Enterprise is configure on the organization as only authencation method
+ // Given idp GitHub Enterprise is configure with account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to GitHub Enterprise
+ // User authenticates in GitHub Enterprise with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with GitHub Enterprise IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub Enterprise is configure on the organization as only authencation method
+ // Given idp GitHub Enterprise is configure with manually account linking not allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to GitHub Enterprise
+ // User authenticates in GitHub Enterprise with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with GitHub Enterprise IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub Enterprise is configure on the organization as only authencation method
+ // Given idp GitHub Enterprise is configure with manually account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to GitHub Enterprise
+ // User authenticates in GitHub Enterprise with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-github.spec.ts b/apps/login/acceptance/tests/idp-github.spec.ts
new file mode 100644
index 0000000000..689e040537
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-github.spec.ts
@@ -0,0 +1,103 @@
+import test from "@playwright/test";
+
+test("login with GitHub IDP", async ({ page }) => {
+ test.skip();
+ // Given a GitHub IDP is configured on the organization
+ // Given the user has GitHub IDP added as auth method
+ // User authenticates with the GitHub IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with GitHub IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the GitHub IDP is configured on the organization
+ // Given the user has GitHub IDP added as auth method
+ // User is redirected to the GitHub IDP
+ // User authenticates with the GitHub IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with GitHub IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub is configure on the organization as only authencation method
+ // Given idp GitHub is configure with account creation alloweed, and automatic creation enabled
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given no user exists yet
+ // User is automatically redirected to GitHub
+ // User authenticates in GitHub
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with GitHub IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub is configure on the organization as only authencation method
+ // Given idp GitHub is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to GitHub
+ // User authenticates in GitHub
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with GitHub IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp GitHub is configure on the organization as only authencation method
+ // Given idp GitHub is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to GitHub
+ // User authenticates in GitHub
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with GitHub IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub is configure on the organization as only authencation method
+ // Given idp GitHub is configure with account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to GitHub
+ // User authenticates in GitHub with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with GitHub IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub is configure on the organization as only authencation method
+ // Given idp GitHub is configure with manually account linking not allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to GitHub
+ // User authenticates in GitHub with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with GitHub IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp GitHub is configure on the organization as only authencation method
+ // Given idp GitHub is configure with manually account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to GitHub
+ // User authenticates in GitHub with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-gitlab-self-hosted.spec.ts b/apps/login/acceptance/tests/idp-gitlab-self-hosted.spec.ts
new file mode 100644
index 0000000000..1b05d5e19b
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-gitlab-self-hosted.spec.ts
@@ -0,0 +1,103 @@
+import test from "@playwright/test";
+
+test("login with GitLab Self-Hosted IDP", async ({ page }) => {
+ test.skip();
+ // Given a GitLab Self-Hosted IDP is configured on the organization
+ // Given the user has GitLab Self-Hosted IDP added as auth method
+ // User authenticates with the GitLab Self-Hosted IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with GitLab Self-Hosted IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the GitLab Self-Hosted IDP is configured on the organization
+ // Given the user has GitLab Self-Hosted IDP added as auth method
+ // User is redirected to the GitLab Self-Hosted IDP
+ // User authenticates with the GitLab Self-Hosted IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Gitlab Self-Hosted IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
+ // Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given no user exists yet
+ // User is automatically redirected to Gitlab Self-Hosted
+ // User authenticates in Gitlab Self-Hosted
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Gitlab Self-Hosted IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
+ // Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Gitlab Self-Hosted
+ // User authenticates in Gitlab Self-Hosted
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Gitlab Self-Hosted IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
+ // Given idp Gitlab Self-Hosted is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Gitlab Self-Hosted
+ // User authenticates in Gitlab Self-Hosted
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Gitlab Self-Hosted IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
+ // Given idp Gitlab Self-Hosted is configure with account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Gitlab Self-Hosted
+ // User authenticates in Gitlab Self-Hosted with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Gitlab Self-Hosted IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
+ // Given idp Gitlab Self-Hosted is configure with manually account linking not allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Gitlab Self-Hosted
+ // User authenticates in Gitlab Self-Hosted with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Gitlab Self-Hosted IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
+ // Given idp Gitlab Self-Hosted is configure with manually account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Gitlab Self-Hosted
+ // User authenticates in Gitlab Self-Hosted with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-gitlab.spec.ts b/apps/login/acceptance/tests/idp-gitlab.spec.ts
new file mode 100644
index 0000000000..fdb235843b
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-gitlab.spec.ts
@@ -0,0 +1,103 @@
+import test from "@playwright/test";
+
+test("login with GitLab IDP", async ({ page }) => {
+ test.skip();
+ // Given a GitLab IDP is configured on the organization
+ // Given the user has GitLab IDP added as auth method
+ // User authenticates with the GitLab IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with GitLab IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the GitLab IDP is configured on the organization
+ // Given the user has GitLab IDP added as auth method
+ // User is redirected to the GitLab IDP
+ // User authenticates with the GitLab IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Gitlab IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab is configure on the organization as only authencation method
+ // Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given no user exists yet
+ // User is automatically redirected to Gitlab
+ // User authenticates in Gitlab
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Gitlab IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab is configure on the organization as only authencation method
+ // Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Gitlab
+ // User authenticates in Gitlab
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Gitlab IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Gitlab is configure on the organization as only authencation method
+ // Given idp Gitlab is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Gitlab
+ // User authenticates in Gitlab
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Gitlab IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab is configure on the organization as only authencation method
+ // Given idp Gitlab is configure with account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Gitlab
+ // User authenticates in Gitlab with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Gitlab IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab is configure on the organization as only authencation method
+ // Given idp Gitlab is configure with manually account linking not allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Gitlab
+ // User authenticates in Gitlab with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Gitlab IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Gitlab is configure on the organization as only authencation method
+ // Given idp Gitlab is configure with manually account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Gitlab
+ // User authenticates in Gitlab with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-google.spec.ts b/apps/login/acceptance/tests/idp-google.spec.ts
new file mode 100644
index 0000000000..8eb4d54e34
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-google.spec.ts
@@ -0,0 +1,99 @@
+import test from "@playwright/test";
+
+test("login with Google IDP", async ({ page }) => {
+ test.skip();
+ // Given a Google IDP is configured on the organization
+ // Given the user has Google IDP added as auth method
+ // User authenticates with the Google IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with Google IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the Google IDP is configured on the organization
+ // Given the user has Google IDP added as auth method
+ // User is redirected to the Google IDP
+ // User authenticates with the Google IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Google IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Google is configure on the organization as only authencation method
+ // Given idp Google is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Google
+ // User authenticates in Google
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Google IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Google is configure on the organization as only authencation method
+ // Given idp Google is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Google
+ // User authenticates in Google
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Google IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Google is configure on the organization as only authencation method
+ // Given idp Google is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Google
+ // User authenticates in Google
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Google IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Google is configure on the organization as only authencation method
+ // Given idp Google is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Google
+ // User authenticates in Google with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Google IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Google is configure on the organization as only authencation method
+ // Given idp Google is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Google
+ // User authenticates in Google with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Google IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Google is configure on the organization as only authencation method
+ // Given idp Google is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Google
+ // User authenticates in Google with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-ldap.spec.ts b/apps/login/acceptance/tests/idp-ldap.spec.ts
new file mode 100644
index 0000000000..0705ed45f8
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-ldap.spec.ts
@@ -0,0 +1,99 @@
+import test from "@playwright/test";
+
+test("login with LDAP IDP", async ({ page }) => {
+ test.skip();
+ // Given a LDAP IDP is configured on the organization
+ // Given the user has LDAP IDP added as auth method
+ // User authenticates with the LDAP IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with LDAP IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the LDAP IDP is configured on the organization
+ // Given the user has LDAP IDP added as auth method
+ // User is redirected to the LDAP IDP
+ // User authenticates with the LDAP IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with LDAP IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp LDAP is configure on the organization as only authencation method
+ // Given idp LDAP is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to LDAP
+ // User authenticates in LDAP
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with LDAP IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp LDAP is configure on the organization as only authencation method
+ // Given idp LDAP is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to LDAP
+ // User authenticates in LDAP
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with LDAP IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp LDAP is configure on the organization as only authencation method
+ // Given idp LDAP is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to LDAP
+ // User authenticates in LDAP
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with LDAP IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp LDAP is configure on the organization as only authencation method
+ // Given idp LDAP is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to LDAP
+ // User authenticates in LDAP with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with LDAP IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp LDAP is configure on the organization as only authencation method
+ // Given idp LDAP is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to LDAP
+ // User authenticates in LDAP with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with LDAP IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp LDAP is configure on the organization as only authencation method
+ // Given idp LDAP is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to LDAP
+ // User authenticates in LDAP with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-microsoft.spec.ts b/apps/login/acceptance/tests/idp-microsoft.spec.ts
new file mode 100644
index 0000000000..15d67c28aa
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-microsoft.spec.ts
@@ -0,0 +1,102 @@
+// Note for all tests, in case Microsoft doesn't deliver all relevant information per default
+// We should add an action in the needed cases
+
+import test from "@playwright/test";
+
+test("login with Microsoft IDP", async ({ page }) => {
+ test.skip();
+ // Given a Microsoft IDP is configured on the organization
+ // Given the user has Microsoft IDP added as auth method
+ // User authenticates with the Microsoft IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with Microsoft IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the Microsoft IDP is configured on the organization
+ // Given the user has Microsoft IDP added as auth method
+ // User is redirected to the Microsoft IDP
+ // User authenticates with the Microsoft IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with Microsoft IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp Microsoft is configure on the organization as only authencation method
+ // Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Microsoft
+ // User authenticates in Microsoft
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Microsoft IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Microsoft is configure on the organization as only authencation method
+ // Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Microsoft
+ // User authenticates in Microsoft
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Microsoft IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp Microsoft is configure on the organization as only authencation method
+ // Given idp Microsoft is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to Microsoft
+ // User authenticates in Microsoft
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with Microsoft IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp Microsoft is configure on the organization as only authencation method
+ // Given idp Microsoft is configure with account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to Microsoft
+ // User authenticates in Microsoft with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with Microsoft IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp Microsoft is configure on the organization as only authencation method
+ // Given idp Microsoft is configure with manually account linking not allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Microsoft
+ // User authenticates in Microsoft with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with Microsoft IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp Microsoft is configure on the organization as only authencation method
+ // Given idp Microsoft is configure with manually account linking allowed, and linking set to existing email
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to Microsoft
+ // User authenticates in Microsoft with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/idp-saml.spec.ts b/apps/login/acceptance/tests/idp-saml.spec.ts
new file mode 100644
index 0000000000..90d8d618b4
--- /dev/null
+++ b/apps/login/acceptance/tests/idp-saml.spec.ts
@@ -0,0 +1,103 @@
+import test from "@playwright/test";
+
+test("login with SAML IDP", async ({ page }) => {
+ test.skip();
+ // Given a SAML IDP is configured on the organization
+ // Given the user has SAML IDP added as auth method
+ // User authenticates with the SAML IDP
+ // User is redirected back to login
+ // User is redirected to the app
+});
+
+test("login with SAML IDP - error", async ({ page }) => {
+ test.skip();
+ // Given the SAML IDP is configured on the organization
+ // Given the user has SAML IDP added as auth method
+ // User is redirected to the SAML IDP
+ // User authenticates with the SAML IDP and gets an error
+ // User is redirected back to login
+ // An error is shown to the user "Something went wrong"
+});
+
+test("login with SAML IDP, no user existing - auto register", async ({ page }) => {
+ test.skip();
+ // Given idp SAML is configure on the organization as only authencation method
+ // Given idp SAML is configure with account creation alloweed, and automatic creation enabled
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given no user exists yet
+ // User is automatically redirected to SAML
+ // User authenticates in SAML
+ // User is redirect to ZITADEL login
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with SAML IDP, no user existing - auto register not possible", async ({ page }) => {
+ test.skip();
+ // Given idp SAML is configure on the organization as only authencation method
+ // Given idp SAML is configure with account creation alloweed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to SAML
+ // User authenticates in SAML
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // User will see the registration page with pre filled user information
+ // User fills missing information
+ // User clicks register button
+ // User is created in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with SAML IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
+ page,
+}) => {
+ test.skip();
+ // Given idp SAML is configure on the organization as only authencation method
+ // Given idp SAML is configure with account creation not allowed, and automatic creation enabled
+ // Given no user exists yet
+ // User is automatically redirected to SAML
+ // User authenticates in SAML
+ // User is redirect to ZITADEL login
+ // Because of missing informaiton on the user auto creation is not possible
+ // Error message is shown, that registration of the user was not possible due to missing information
+});
+
+test("login with SAML IDP, no user linked - auto link", async ({ page }) => {
+ test.skip();
+ // Given idp SAML is configure on the organization as only authencation method
+ // Given idp SAML is configure with account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com exists
+ // User is automatically redirected to SAML
+ // User authenticates in SAML with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User is linked with existing user in ZITADEL
+ // User is redirected to the app (default redirect url)
+});
+
+test("login with SAML IDP, no user linked, linking not possible", async ({ page }) => {
+ test.skip();
+ // Given idp SAML is configure on the organization as only authencation method
+ // Given idp SAML is configure with manually account linking not allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to SAML
+ // User authenticates in SAML with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User will get an error message that account linking wasn't possible
+});
+
+test("login with SAML IDP, no user linked, linking successful", async ({ page }) => {
+ test.skip();
+ // Given idp SAML is configure on the organization as only authencation method
+ // Given idp SAML is configure with manually account linking allowed, and linking set to existing email
+ // Given ZITADEL Action is added to autofill missing user information
+ // Given user with email address user@zitadel.com doesn't exists
+ // User is automatically redirected to SAML
+ // User authenticates in SAML with user@zitadel.com
+ // User is redirect to ZITADEL login
+ // User with email address user@zitadel.com can not be found
+ // User is prompted to link the account manually
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/login-configuration-possiblities.spec.ts b/apps/login/acceptance/tests/login-configuration-possiblities.spec.ts
new file mode 100644
index 0000000000..cc58dbcc71
--- /dev/null
+++ b/apps/login/acceptance/tests/login-configuration-possiblities.spec.ts
@@ -0,0 +1,57 @@
+import test from "@playwright/test";
+
+test("login with mfa setup, mfa setup prompt", async ({ page }) => {
+ test.skip();
+ // Given the organization has enabled at least one mfa types
+ // Given the user has a password but no mfa registered
+ // User authenticates with login name and password
+ // User is prompted to setup a mfa, mfa providers are listed, the user can choose the provider
+});
+
+test("login with mfa setup, no mfa setup prompt", async ({ page }) => {
+ test.skip();
+ // Given the organization has set "multifactor init check time" to 0
+ // Given the organization has enabled mfa types
+ // Given the user has a password but no mfa registered
+ // User authenticates with loginname and password
+ // user is directly loged in and not prompted to setup mfa
+});
+
+test("login with mfa setup, force mfa for local authenticated users", async ({ page }) => {
+ test.skip();
+ // Given the organization has enabled force mfa for local authentiacted users
+ // Given the organization has enabled all possible mfa types
+ // Given the user has a password but no mfa registered
+ // User authenticates with loginname and password
+ // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
+});
+
+test("login with mfa setup, force mfa - local user", async ({ page }) => {
+ test.skip();
+ // Given the organization has enabled force mfa for local authentiacted users
+ // Given the organization has enabled all possible mfa types
+ // Given the user has a password but no mfa registered
+ // User authenticates with loginname and password
+ // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
+});
+
+test("login with mfa setup, force mfa - external user", async ({ page }) => {
+ test.skip();
+ // Given the organization has enabled force mfa
+ // Given the organization has enabled all possible mfa types
+ // Given the user has an idp but no mfa registered
+ // enter login name
+ // redirect to configured external idp
+ // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
+});
+
+test("login with mfa setup, force mfa - local user, wrong password", async ({ page }) => {
+ test.skip();
+ // Given the organization has a password lockout policy set to 1 on the max password attempts
+ // Given the user has only a password as auth methos
+ // enter login name
+ // enter wrong password
+ // User will get an error "Wrong password"
+ // enter password
+ // User will get an error "Max password attempts reached - user is locked. Please reach out to your administrator"
+});
diff --git a/apps/login/acceptance/tests/login.ts b/apps/login/acceptance/tests/login.ts
new file mode 100644
index 0000000000..2076412456
--- /dev/null
+++ b/apps/login/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/apps/login/acceptance/tests/loginname-screen.ts b/apps/login/acceptance/tests/loginname-screen.ts
new file mode 100644
index 0000000000..be41a28eda
--- /dev/null
+++ b/apps/login/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/apps/login/acceptance/tests/loginname.ts b/apps/login/acceptance/tests/loginname.ts
new file mode 100644
index 0000000000..2050ec1d3c
--- /dev/null
+++ b/apps/login/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/apps/login/acceptance/tests/passkey.ts b/apps/login/acceptance/tests/passkey.ts
new file mode 100644
index 0000000000..d8cda10ddb
--- /dev/null
+++ b/apps/login/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/apps/login/acceptance/tests/password-screen.ts b/apps/login/acceptance/tests/password-screen.ts
new file mode 100644
index 0000000000..fda6f6d39f
--- /dev/null
+++ b/apps/login/acceptance/tests/password-screen.ts
@@ -0,0 +1,98 @@
+import { expect, Page } from "@playwright/test";
+import { getCodeFromSink } from "./sink";
+
+const codeField = "code-text-input";
+const passwordField = "password-text-input";
+const passwordChangeField = "password-change-text-input";
+const passwordChangeConfirmField = "password-change-confirm-text-input";
+const passwordSetField = "password-set-text-input";
+const passwordSetConfirmField = "password-set-confirm-text-input";
+const lengthCheck = "length-check";
+const symbolCheck = "symbol-check";
+const numberCheck = "number-check";
+const uppercaseCheck = "uppercase-check";
+const lowercaseCheck = "lowercase-check";
+const equalCheck = "equal-check";
+
+const matchText = "Matches";
+const noMatchText = "Doesn't match";
+
+export async function changePasswordScreen(page: Page, password1: string, password2: string) {
+ await page.getByTestId(passwordChangeField).pressSequentially(password1);
+ await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2);
+}
+
+export async function passwordScreen(page: Page, password: string) {
+ await page.getByTestId(passwordField).pressSequentially(password);
+}
+
+export async function passwordScreenExpect(page: Page, password: string) {
+ await expect(page.getByTestId(passwordField)).toHaveValue(password);
+ await expect(page.getByTestId("error").locator("div")).toContainText("Failed to authenticate.");
+}
+
+export async function changePasswordScreenExpect(
+ page: Page,
+ password1: string,
+ password2: string,
+ length: boolean,
+ symbol: boolean,
+ number: boolean,
+ uppercase: boolean,
+ lowercase: boolean,
+ equals: boolean,
+) {
+ await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1);
+ await expect(page.getByTestId(passwordChangeConfirmField)).toHaveValue(password2);
+
+ await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
+}
+
+async function checkComplexity(
+ page: Page,
+ length: boolean,
+ symbol: boolean,
+ number: boolean,
+ uppercase: boolean,
+ lowercase: boolean,
+ equals: boolean,
+) {
+ await checkContent(page, lengthCheck, length);
+ await checkContent(page, symbolCheck, symbol);
+ await checkContent(page, numberCheck, number);
+ await checkContent(page, uppercaseCheck, uppercase);
+ await checkContent(page, lowercaseCheck, lowercase);
+ await checkContent(page, equalCheck, equals);
+}
+
+async function checkContent(page: Page, testid: string, match: boolean) {
+ if (match) {
+ await expect(page.getByTestId(testid)).toContainText(matchText);
+ } else {
+ await expect(page.getByTestId(testid)).toContainText(noMatchText);
+ }
+}
+
+export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) {
+ const c = await getCodeFromSink(username);
+ await page.getByTestId(codeField).pressSequentially(c);
+ await page.getByTestId(passwordSetField).pressSequentially(password1);
+ await page.getByTestId(passwordSetConfirmField).pressSequentially(password2);
+}
+
+export async function resetPasswordScreenExpect(
+ page: Page,
+ password1: string,
+ password2: string,
+ length: boolean,
+ symbol: boolean,
+ number: boolean,
+ uppercase: boolean,
+ lowercase: boolean,
+ equals: boolean,
+) {
+ await expect(page.getByTestId(passwordSetField)).toHaveValue(password1);
+ await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2);
+
+ await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
+}
diff --git a/apps/login/acceptance/tests/password.ts b/apps/login/acceptance/tests/password.ts
new file mode 100644
index 0000000000..ccf3e509d9
--- /dev/null
+++ b/apps/login/acceptance/tests/password.ts
@@ -0,0 +1,29 @@
+import { Page } from "@playwright/test";
+import { changePasswordScreen, passwordScreen, resetPasswordScreen } from "./password-screen";
+
+const passwordSubmitButton = "submit-button";
+const passwordResetButton = "reset-button";
+
+export async function startChangePassword(page: Page, loginname: string) {
+ await page.goto("./password/change?" + new URLSearchParams({ loginName: loginname }));
+}
+
+export async function changePassword(page: Page, password: string) {
+ await changePasswordScreen(page, password, password);
+ await page.getByTestId(passwordSubmitButton).click();
+}
+
+export async function password(page: Page, password: string) {
+ await passwordScreen(page, password);
+ await page.getByTestId(passwordSubmitButton).click();
+}
+
+export async function startResetPassword(page: Page) {
+ await page.getByTestId(passwordResetButton).click();
+}
+
+export async function resetPassword(page: Page, username: string, password: string) {
+ await startResetPassword(page);
+ await resetPasswordScreen(page, username, password, password);
+ await page.getByTestId(passwordSubmitButton).click();
+}
diff --git a/apps/login/acceptance/tests/register-screen.ts b/apps/login/acceptance/tests/register-screen.ts
new file mode 100644
index 0000000000..d14f5dc970
--- /dev/null
+++ b/apps/login/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/apps/login/acceptance/tests/register.spec.ts b/apps/login/acceptance/tests/register.spec.ts
new file mode 100644
index 0000000000..4ad7e9e349
--- /dev/null
+++ b/apps/login/acceptance/tests/register.spec.ts
@@ -0,0 +1,183 @@
+import { faker } from "@faker-js/faker";
+import { test } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginScreenExpect } from "./login";
+import { registerWithPasskey, registerWithPassword } from "./register";
+import { removeUserByUsername } from "./zitadel";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+test("register with password", async ({ page }) => {
+ const username = faker.internet.email();
+ const password = "Password1!";
+ const firstname = faker.person.firstName();
+ const lastname = faker.person.lastName();
+
+ await registerWithPassword(page, firstname, lastname, username, password, password);
+ await loginScreenExpect(page, firstname + " " + lastname);
+
+ // wait for projection of user
+ await page.waitForTimeout(10000);
+ await removeUserByUsername(username);
+});
+
+test("register with passkey", async ({ page }) => {
+ const username = faker.internet.email();
+ const firstname = faker.person.firstName();
+ const lastname = faker.person.lastName();
+
+ await registerWithPasskey(page, firstname, lastname, username);
+ await loginScreenExpect(page, firstname + " " + lastname);
+
+ // wait for projection of user
+ await page.waitForTimeout(10000);
+ await removeUserByUsername(username);
+});
+
+test("register with username and password - only password enabled", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is not enabled
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // Only password is shown as an option - no passkey
+ // User enters "firstname", "lastname", "username" and "password"
+ // User is redirected to app (default redirect url)
+});
+
+test("register with username and password - wrong password not enough characters", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is not enabled
+ // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // Only password is shown as an option - no passkey
+ // User enters "firstname", "lastname", "username" and a password thats to short
+ // Error is shown "Password doesn't match the policy - it must have at least 8 characters"
+});
+
+test("register with username and password - wrong password number missing", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is not enabled
+ // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // Only password is shown as an option - no passkey
+ // User enters "firstname", "lastname", "username" and a password without a number
+ // Error is shown "Password doesn't match the policy - number missing"
+});
+
+test("register with username and password - wrong password upper case missing", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is not enabled
+ // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // Only password is shown as an option - no passkey
+ // User enters "firstname", "lastname", "username" and a password without an upper case
+ // Error is shown "Password doesn't match the policy - uppercase letter missing"
+});
+
+test("register with username and password - wrong password lower case missing", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is not enabled
+ // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // Only password is shown as an option - no passkey
+ // User enters "firstname", "lastname", "username" and a password without an lower case
+ // Error is shown "Password doesn't match the policy - lowercase letter missing"
+});
+
+test("register with username and password - wrong password symboo missing", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is not enabled
+ // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // Only password is shown as an option - no passkey
+ // User enters "firstname", "lastname", "username" and a password without an symbol
+ // Error is shown "Password doesn't match the policy - symbol missing"
+});
+
+test("register with username and password - password and passkey enabled", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is enabled
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // User enters "firstname", "lastname", "username"
+ // Password and passkey are shown as authentication option
+ // User clicks password
+ // User enters password
+ // User is redirected to app (default redirect url)
+});
+
+test("register with username and passkey - password and passkey enabled", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given on the default organization passkey is enabled
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration page
+ // User enters "firstname", "lastname", "username"
+ // Password and passkey are shown as authentication option
+ // User clicks passkey
+ // Passkey is opened automatically
+ // User verifies passkey
+ // User is redirected to app (default redirect url)
+});
+
+test("register with username and password - registration disabled", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization no idp is configured and enabled
+ // Given user doesn't exist
+ // Button "register new user" is not available
+});
+
+test("register with username and password - multiple registration options", async ({ page }) => {
+ test.skip();
+ // Given on the default organization "username and password is allowed" is enabled
+ // Given on the default organization "username registeration allowed" is enabled
+ // Given on the default organization one idp is configured and enabled
+ // Given user doesn't exist
+ // Click on button "register new user"
+ // User is redirected to registration options
+ // Local User and idp button are shown
+ // User clicks idp button
+ // User enters "firstname", "lastname", "username" and "password"
+ // User clicks next
+ // User is redirected to app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/register.ts b/apps/login/acceptance/tests/register.ts
new file mode 100644
index 0000000000..164a72753b
--- /dev/null
+++ b/apps/login/acceptance/tests/register.ts
@@ -0,0 +1,39 @@
+import { Page } from "@playwright/test";
+import { emailVerify } from "./email-verify";
+import { passkeyRegister } from "./passkey";
+import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
+import { getCodeFromSink } from "./sink";
+
+export async function registerWithPassword(
+ page: Page,
+ firstname: string,
+ lastname: string,
+ email: string,
+ password1: string,
+ password2: string,
+) {
+ await page.goto("./register");
+ await registerUserScreenPassword(page, firstname, lastname, email);
+ await page.getByTestId("submit-button").click();
+ await registerPasswordScreen(page, password1, password2);
+ await page.getByTestId("submit-button").click();
+ await verifyEmail(page, email);
+}
+
+export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise {
+ await page.goto("./register");
+ await registerUserScreenPasskey(page, firstname, lastname, email);
+ await page.getByTestId("submit-button").click();
+
+ // wait for projection of user
+ await page.waitForTimeout(10000);
+ const authId = await passkeyRegister(page);
+
+ await verifyEmail(page, email);
+ return authId;
+}
+
+async function verifyEmail(page: Page, email: string) {
+ const c = await getCodeFromSink(email);
+ await emailVerify(page, c);
+}
diff --git a/apps/login/acceptance/tests/select-account.ts b/apps/login/acceptance/tests/select-account.ts
new file mode 100644
index 0000000000..64bd7cd145
--- /dev/null
+++ b/apps/login/acceptance/tests/select-account.ts
@@ -0,0 +1,5 @@
+import { Page } from "@playwright/test";
+
+export async function selectNewAccount(page: Page) {
+ await page.getByRole("link", { name: "Add another account" }).click();
+}
diff --git a/apps/login/acceptance/tests/sink.ts b/apps/login/acceptance/tests/sink.ts
new file mode 100644
index 0000000000..bc3336b358
--- /dev/null
+++ b/apps/login/acceptance/tests/sink.ts
@@ -0,0 +1,43 @@
+import { Gaxios, GaxiosResponse } from "gaxios";
+
+const awaitNotification = new Gaxios({
+ url: process.env.SINK_NOTIFICATION_URL,
+ method: "POST",
+ retryConfig: {
+ httpMethodsToRetry: ["POST"],
+ statusCodesToRetry: [[404, 404]],
+ retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
+ totalTimeout: 10000, // 10 seconds
+ onRetryAttempt: (error) => {
+ console.warn(`Retrying request to sink notification service: ${error.message}`);
+ },
+ },
+});
+
+export async function getOtpFromSink(recipient: string): Promise {
+ return awaitNotification.request({ data: { recipient } }).then((response) => {
+ expectSuccess(response);
+ const otp = response?.data?.args?.otp;
+ if (!otp) {
+ throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
+ }
+ return otp;
+ });
+}
+
+export async function getCodeFromSink(recipient: string): Promise {
+ return awaitNotification.request({ data: { recipient } }).then((response) => {
+ expectSuccess(response);
+ const code = response?.data?.args?.code;
+ if (!code) {
+ throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`);
+ }
+ return code;
+ });
+}
+
+function expectSuccess(response: GaxiosResponse): void {
+ if (response.status !== 200) {
+ throw new Error(`Expected HTTP status 200, but got: ${response.status} - ${response.statusText}`);
+ }
+}
diff --git a/apps/login/acceptance/tests/user.ts b/apps/login/acceptance/tests/user.ts
new file mode 100644
index 0000000000..3b03291408
--- /dev/null
+++ b/apps/login/acceptance/tests/user.ts
@@ -0,0 +1,177 @@
+import { Page } from "@playwright/test";
+import { registerWithPasskey } from "./register";
+import { activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser } from "./zitadel";
+
+export interface userProps {
+ email: string;
+ isEmailVerified?: boolean;
+ firstName: string;
+ lastName: string;
+ organization: string;
+ password: string;
+ passwordChangeRequired?: boolean;
+ phone: string;
+ isPhoneVerified?: boolean;
+}
+
+class User {
+ private readonly props: userProps;
+ private user: string;
+
+ constructor(userProps: userProps) {
+ this.props = userProps;
+ }
+
+ async ensure(page: Page) {
+ const response = await addUser(this.props);
+
+ this.setUserId(response.userId);
+ }
+
+ async cleanup() {
+ await removeUser(this.getUserId());
+ }
+
+ public setUserId(userId: string) {
+ this.user = userId;
+ }
+
+ public getUserId() {
+ return this.user;
+ }
+
+ public getUsername() {
+ return this.props.email;
+ }
+
+ public getPassword() {
+ return this.props.password;
+ }
+
+ public getFirstname() {
+ return this.props.firstName;
+ }
+
+ public getLastname() {
+ return this.props.lastName;
+ }
+
+ public getPhone() {
+ return this.props.phone;
+ }
+
+ public getFullName() {
+ return `${this.props.firstName} ${this.props.lastName}`;
+ }
+}
+
+export class PasswordUser extends User {
+ async ensure(page: Page) {
+ await super.ensure(page);
+ await eventualNewUser(this.getUserId());
+ }
+}
+
+export enum OtpType {
+ sms = "sms",
+ email = "email",
+}
+
+export interface otpUserProps {
+ email: string;
+ isEmailVerified?: boolean;
+ firstName: string;
+ lastName: string;
+ organization: string;
+ password: string;
+ passwordChangeRequired?: boolean;
+ phone: string;
+ isPhoneVerified?: boolean;
+ type: OtpType;
+}
+
+export class PasswordUserWithOTP extends User {
+ private type: OtpType;
+
+ constructor(props: otpUserProps) {
+ super({
+ email: props.email,
+ firstName: props.firstName,
+ lastName: props.lastName,
+ organization: props.organization,
+ password: props.password,
+ phone: props.phone,
+ isEmailVerified: props.isEmailVerified,
+ isPhoneVerified: props.isPhoneVerified,
+ passwordChangeRequired: props.passwordChangeRequired,
+ });
+ this.type = props.type;
+ }
+
+ async ensure(page: Page) {
+ await super.ensure(page);
+ await activateOTP(this.getUserId(), this.type);
+ await eventualNewUser(this.getUserId());
+ }
+}
+
+export class PasswordUserWithTOTP extends User {
+ private secret: string;
+
+ async ensure(page: Page) {
+ await super.ensure(page);
+ this.secret = await addTOTP(this.getUserId());
+ await eventualNewUser(this.getUserId());
+ }
+
+ public getSecret(): string {
+ return this.secret;
+ }
+}
+
+export interface passkeyUserProps {
+ email: string;
+ firstName: string;
+ lastName: string;
+ organization: string;
+ phone: string;
+ isEmailVerified?: boolean;
+ isPhoneVerified?: boolean;
+}
+
+export class PasskeyUser extends User {
+ private authenticatorId: string;
+
+ constructor(props: passkeyUserProps) {
+ super({
+ email: props.email,
+ firstName: props.firstName,
+ lastName: props.lastName,
+ organization: props.organization,
+ password: "",
+ phone: props.phone,
+ isEmailVerified: props.isEmailVerified,
+ isPhoneVerified: props.isPhoneVerified,
+ });
+ }
+
+ public async ensure(page: Page) {
+ const authId = await registerWithPasskey(page, this.getFirstname(), this.getLastname(), this.getUsername());
+ this.authenticatorId = authId;
+
+ // wait for projection of user
+ await page.waitForTimeout(10000);
+ }
+
+ async cleanup() {
+ const resp: any = await getUserByUsername(this.getUsername());
+ if (!resp || !resp.result || !resp.result[0]) {
+ return;
+ }
+ await removeUser(resp.result[0].userId);
+ }
+
+ public getAuthenticatorId(): string {
+ return this.authenticatorId;
+ }
+}
diff --git a/apps/login/acceptance/tests/username-passkey.spec.ts b/apps/login/acceptance/tests/username-passkey.spec.ts
new file mode 100644
index 0000000000..dff1c65f5a
--- /dev/null
+++ b/apps/login/acceptance/tests/username-passkey.spec.ts
@@ -0,0 +1,43 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginScreenExpect, loginWithPasskey } from "./login";
+import { PasskeyUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasskeyUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasskeyUser({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and passkey login", async ({ user, page }) => {
+ await loginWithPasskey(page, user.getAuthenticatorId(), user.getUsername());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("username and passkey login, multiple auth methods", async ({ page }) => {
+ test.skip();
+ // Given passkey and password is enabled on the organization of the user
+ // Given the user has password and passkey registered
+ // enter username
+ // passkey popup is directly shown
+ // user aborts passkey authentication
+ // user switches to password authentication
+ // user enters password
+ // user is redirected to app
+});
diff --git a/apps/login/acceptance/tests/username-password-change-required.spec.ts b/apps/login/acceptance/tests/username-password-change-required.spec.ts
new file mode 100644
index 0000000000..50605e5ff0
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-change-required.spec.ts
@@ -0,0 +1,41 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginScreenExpect, loginWithPassword } from "./login";
+import { changePassword } from "./password";
+import { PasswordUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ password: "Password1!",
+ passwordChangeRequired: true,
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and password login, change required", async ({ user, page }) => {
+ const changedPw = "ChangedPw1!";
+
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await page.waitForTimeout(10000);
+ await changePassword(page, changedPw);
+ await loginScreenExpect(page, user.getFullName());
+
+ await loginWithPassword(page, user.getUsername(), changedPw);
+ await loginScreenExpect(page, user.getFullName());
+});
diff --git a/apps/login/acceptance/tests/username-password-changed.spec.ts b/apps/login/acceptance/tests/username-password-changed.spec.ts
new file mode 100644
index 0000000000..dc29dc2286
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-changed.spec.ts
@@ -0,0 +1,54 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginScreenExpect, loginWithPassword } from "./login";
+import { changePassword, startChangePassword } from "./password";
+import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
+import { PasswordUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and password changed login", async ({ user, page }) => {
+ const changedPw = "ChangedPw1!";
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+
+ // wait for projection of token
+ await page.waitForTimeout(10000);
+
+ await startChangePassword(page, user.getUsername());
+ await changePassword(page, changedPw);
+ await loginScreenExpect(page, user.getFullName());
+
+ await loginWithPassword(page, user.getUsername(), changedPw);
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("password change not with desired complexity", async ({ user, page }) => {
+ const changedPw1 = "change";
+ const changedPw2 = "chang";
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await startChangePassword(page, user.getUsername());
+ await changePasswordScreen(page, changedPw1, changedPw2);
+ await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
+});
diff --git a/apps/login/acceptance/tests/username-password-otp_email.spec.ts b/apps/login/acceptance/tests/username-password-otp_email.spec.ts
new file mode 100644
index 0000000000..e4a77751c1
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-otp_email.spec.ts
@@ -0,0 +1,98 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { code, codeResend, otpFromSink } from "./code";
+import { codeScreenExpect } from "./code-screen";
+import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } from "./login";
+import { OtpType, PasswordUserWithOTP } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUserWithOTP({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ type: OtpType.email,
+ });
+
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test.skip("DOESN'T WORK: username, password and email otp login, enter code manually", async ({ user, page }) => {
+ // Given email otp is enabled on the organization of the user
+ // Given the user has only email otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives an email with a verification code
+ // User enters the code into the ui
+ // User is redirected to the app (default redirect url)
+ await loginWithPasswordAndEmailOTP(page, user.getUsername(), user.getPassword(), user.getUsername());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("username, password and email otp login, click link in email", async ({ page }) => {
+ base.skip();
+ // Given email otp is enabled on the organization of the user
+ // Given the user has only email otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives an email with a verification code
+ // User clicks link in the email
+ // User is redirected to the app (default redirect url)
+});
+
+test.skip("DOESN'T WORK: username, password and email otp login, resend code", async ({ user, page }) => {
+ // Given email otp is enabled on the organization of the user
+ // Given the user has only email otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives an email with a verification code
+ // User clicks resend code
+ // User receives a new email with a verification code
+ // User enters the new code in the ui
+ // User is redirected to the app (default redirect url)
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await codeResend(page);
+ await otpFromSink(page, user.getUsername());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("username, password and email otp login, wrong code", async ({ user, page }) => {
+ // Given email otp is enabled on the organization of the user
+ // Given the user has only email otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives an email with a verification code
+ // User enters a wrong code
+ // Error message - "Invalid code" is shown
+ const c = "wrongcode";
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await code(page, c);
+ await codeScreenExpect(page, c);
+});
+
+test("username, password and email otp login, multiple mfa options", async ({ page }) => {
+ base.skip();
+ // Given email otp and sms otp is enabled on the organization of the user
+ // Given the user has email and sms otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives an email with a verification code
+ // User clicks button to use sms otp as second factor
+ // User receives a sms with a verification code
+ // User enters code in ui
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/username-password-otp_sms.spec.ts b/apps/login/acceptance/tests/username-password-otp_sms.spec.ts
new file mode 100644
index 0000000000..10901cd243
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-otp_sms.spec.ts
@@ -0,0 +1,71 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { code } from "./code";
+import { codeScreenExpect } from "./code-screen";
+import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } from "./login";
+import { OtpType, PasswordUserWithOTP } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUserWithOTP({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number({ style: "international" }),
+ isPhoneVerified: true,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ type: OtpType.sms,
+ });
+
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test.skip("DOESN'T WORK: username, password and sms otp login, enter code manually", async ({ user, page }) => {
+ // Given sms otp is enabled on the organization of the user
+ // Given the user has only sms otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives a sms with a verification code
+ // User enters the code into the ui
+ // User is redirected to the app (default redirect url)
+ await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test.skip("DOESN'T WORK: username, password and sms otp login, resend code", async ({ user, page }) => {
+ // Given sms otp is enabled on the organization of the user
+ // Given the user has only sms otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives a sms with a verification code
+ // User clicks resend code
+ // User receives a new sms with a verification code
+ // User is redirected to the app (default redirect url)
+ await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("username, password and sms otp login, wrong code", async ({ user, page }) => {
+ // Given sms otp is enabled on the organization of the user
+ // Given the user has only sms otp configured as second factor
+ // User enters username
+ // User enters password
+ // User receives a sms with a verification code
+ // User enters a wrong code
+ // Error message - "Invalid code" is shown
+ const c = "wrongcode";
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await code(page, c);
+ await codeScreenExpect(page, c);
+});
diff --git a/apps/login/acceptance/tests/username-password-set.spec.ts b/apps/login/acceptance/tests/username-password-set.spec.ts
new file mode 100644
index 0000000000..06ce42f1a7
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-set.spec.ts
@@ -0,0 +1,52 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginScreenExpect, loginWithPassword, startLogin } from "./login";
+import { loginname } from "./loginname";
+import { resetPassword, startResetPassword } from "./password";
+import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen";
+import { PasswordUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and password set login", async ({ user, page }) => {
+ const changedPw = "ChangedPw1!";
+ await startLogin(page);
+ await loginname(page, user.getUsername());
+ await resetPassword(page, user.getUsername(), changedPw);
+ await loginScreenExpect(page, user.getFullName());
+
+ await loginWithPassword(page, user.getUsername(), changedPw);
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("password set not with desired complexity", async ({ user, page }) => {
+ const changedPw1 = "change";
+ const changedPw2 = "chang";
+ await startLogin(page);
+ await loginname(page, user.getUsername());
+ await startResetPassword(page);
+ await resetPasswordScreen(page, user.getUsername(), changedPw1, changedPw2);
+ await resetPasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
+});
diff --git a/apps/login/acceptance/tests/username-password-totp.spec.ts b/apps/login/acceptance/tests/username-password-totp.spec.ts
new file mode 100644
index 0000000000..e495b16681
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-totp.spec.ts
@@ -0,0 +1,71 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { code } from "./code";
+import { codeScreenExpect } from "./code-screen";
+import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "./login";
+import { PasswordUserWithTOTP } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUserWithTOTP({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number({ style: "international" }),
+ isPhoneVerified: true,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ });
+
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username, password and totp login", async ({ user, page }) => {
+ // Given totp is enabled on the organization of the user
+ // Given the user has only totp configured as second factor
+ // User enters username
+ // User enters password
+ // Screen for entering the code is shown directly
+ // User enters the code into the ui
+ // User is redirected to the app (default redirect url)
+ await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("username, password and totp otp login, wrong code", async ({ user, page }) => {
+ // Given totp is enabled on the organization of the user
+ // Given the user has only totp configured as second factor
+ // User enters username
+ // User enters password
+ // Screen for entering the code is shown directly
+ // User enters a wrond code
+ // Error message - "Invalid code" is shown
+ const c = "wrongcode";
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await code(page, c);
+ await codeScreenExpect(page, c);
+});
+
+test("username, password and totp login, multiple mfa options", async ({ page }) => {
+ test.skip();
+ // Given totp and email otp is enabled on the organization of the user
+ // Given the user has totp and email otp configured as second factor
+ // User enters username
+ // User enters password
+ // Screen for entering the code is shown directly
+ // Button to switch to email otp is shown
+ // User clicks button to use email otp instead
+ // User receives an email with a verification code
+ // User enters code in ui
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/username-password-u2f.spec.ts b/apps/login/acceptance/tests/username-password-u2f.spec.ts
new file mode 100644
index 0000000000..dc23064fd6
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password-u2f.spec.ts
@@ -0,0 +1,26 @@
+import { test } from "@playwright/test";
+
+test("username, password and u2f login", async ({ page }) => {
+ test.skip();
+ // Given u2f is enabled on the organization of the user
+ // Given the user has only u2f configured as second factor
+ // User enters username
+ // User enters password
+ // Popup for u2f is directly opened
+ // User verifies u2f
+ // User is redirected to the app (default redirect url)
+});
+
+test("username, password and u2f login, multiple mfa options", async ({ page }) => {
+ test.skip();
+ // Given u2f and semailms otp is enabled on the organization of the user
+ // Given the user has u2f and email otp configured as second factor
+ // User enters username
+ // User enters password
+ // Popup for u2f is directly opened
+ // User aborts u2f verification
+ // User clicks button to use email otp as second factor
+ // User receives an email with a verification code
+ // User enters code in ui
+ // User is redirected to the app (default redirect url)
+});
diff --git a/apps/login/acceptance/tests/username-password.spec.ts b/apps/login/acceptance/tests/username-password.spec.ts
new file mode 100644
index 0000000000..ceb340f8da
--- /dev/null
+++ b/apps/login/acceptance/tests/username-password.spec.ts
@@ -0,0 +1,157 @@
+import { faker } from "@faker-js/faker";
+import { test as base } from "@playwright/test";
+import dotenv from "dotenv";
+import path from "path";
+import { loginScreenExpect, loginWithPassword, startLogin } from "./login";
+import { loginname } from "./loginname";
+import { loginnameScreenExpect } from "./loginname-screen";
+import { password } from "./password";
+import { passwordScreenExpect } from "./password-screen";
+import { PasswordUser } from "./user";
+
+// Read from ".env" file.
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+const test = base.extend<{ user: PasswordUser }>({
+ user: async ({ page }, use) => {
+ const user = new PasswordUser({
+ email: faker.internet.email(),
+ isEmailVerified: true,
+ firstName: faker.person.firstName(),
+ lastName: faker.person.lastName(),
+ organization: "",
+ phone: faker.phone.number(),
+ isPhoneVerified: false,
+ password: "Password1!",
+ passwordChangeRequired: false,
+ });
+ await user.ensure(page);
+ await use(user);
+ await user.cleanup();
+ },
+});
+
+test("username and password login", async ({ user, page }) => {
+ await loginWithPassword(page, user.getUsername(), user.getPassword());
+ await loginScreenExpect(page, user.getFullName());
+});
+
+test("username and password login, unknown username", async ({ page }) => {
+ const username = "unknown";
+ await startLogin(page);
+ await loginname(page, username);
+ await loginnameScreenExpect(page, username);
+});
+
+test("username and password login, wrong password", async ({ user, page }) => {
+ await startLogin(page);
+ await loginname(page, user.getUsername());
+ await password(page, "wrong");
+ await passwordScreenExpect(page, "wrong");
+});
+
+test("username and password login, wrong username, ignore unknown usernames", async ({ user, page }) => {
+ test.skip();
+ // Given user doesn't exist but ignore unknown usernames setting is set to true
+ // Given username password login is enabled on the users organization
+ // enter login name
+ // enter password
+ // redirect to loginname page --> error message username or password wrong
+});
+
+test("username and password login, initial password change", async ({ user, page }) => {
+ test.skip();
+ // Given user is created and has changePassword set to true
+ // Given username password login is enabled on the users organization
+ // enter login name
+ // enter password
+ // create new password
+});
+
+test("username and password login, reset password hidden", async ({ user, page }) => {
+ test.skip();
+ // Given the organization has enabled "Password reset hidden" in the login policy
+ // Given username password login is enabled on the users organization
+ // enter login name
+ // password reset link should not be shown on password screen
+});
+
+test("username and password login, reset password - enter code manually", async ({ user, page }) => {
+ test.skip();
+ // Given user has forgotten password and clicks the forgot password button
+ // Given username password login is enabled on the users organization
+ // enter login name
+ // click password forgotten
+ // enter code from email
+ // user is redirected to app (default redirect url)
+});
+
+test("username and password login, reset password - click link", async ({ user, page }) => {
+ test.skip();
+ // Given user has forgotten password and clicks the forgot password button, and then the link in the email
+ // Given username password login is enabled on the users organization
+ // enter login name
+ // click password forgotten
+ // click link in email
+ // set new password
+ // redirect to app (default redirect url)
+});
+
+test("username and password login, reset password, resend code", async ({ user, page }) => {
+ test.skip();
+ // Given user has forgotten password and clicks the forgot password button and then resend code
+ // Given username password login is enabled on the users organization
+ // enter login name
+ // click password forgotten
+ // click resend code
+ // enter code from second email
+ // user is redirected to app (default redirect url)
+});
+
+test("email login enabled", async ({ user, page }) => {
+ test.skip();
+ // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
+ // Given no other user with the same email address exists
+ // enter email address "test@zitadel.com " in login screen
+ // user will get to password screen
+});
+
+test("email login disabled", async ({ user, page }) => {
+ test.skip();
+ // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
+ // Given no other user with the same email address exists
+ // enter email address "test@zitadel.com" in login screen
+ // user will see error message "user not found"
+});
+
+test("email login enabled - multiple users", async ({ user, page }) => {
+ test.skip();
+ // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
+ // Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists
+ // enter email address "test@zitadel.com" in login screen
+ // user will see error message "user not found"
+});
+
+test("phone login enabled", async ({ user, page }) => {
+ test.skip();
+ // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
+ // Given no other user with the same phon number exists
+ // enter phone number "0711111111" in login screen
+ // user will get to password screen
+});
+
+test("phone login disabled", async ({ user, page }) => {
+ test.skip();
+ // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
+ // Given no other user with the same phone number exists
+ // enter phone number "0711111111" in login screen
+ // user will see error message "user not found"
+});
+
+test("phone login enabled - multiple users", async ({ user, page }) => {
+ test.skip();
+ // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
+ // Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists
+ // enter phone number "0711111111" in login screen
+ // user will see error message "user not found"
+});
diff --git a/apps/login/acceptance/tests/welcome.ts b/apps/login/acceptance/tests/welcome.ts
new file mode 100644
index 0000000000..34267c2bd0
--- /dev/null
+++ b/apps/login/acceptance/tests/welcome.ts
@@ -0,0 +1,6 @@
+import { test } from "@playwright/test";
+
+test("login is accessible", async ({ page }) => {
+ await page.goto("./");
+ await page.getByRole("heading", { name: "Welcome back!" }).isVisible();
+});
diff --git a/apps/login/acceptance/tests/zitadel.ts b/apps/login/acceptance/tests/zitadel.ts
new file mode 100644
index 0000000000..b252654f86
--- /dev/null
+++ b/apps/login/acceptance/tests/zitadel.ts
@@ -0,0 +1,190 @@
+import { Authenticator } from "@otplib/core";
+import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
+import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin
+import axios from "axios";
+import dotenv from "dotenv";
+import { request } from "gaxios";
+import path from "path";
+import { OtpType, userProps } from "./user";
+
+dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
+
+export async function addUser(props: userProps) {
+ const body = {
+ username: props.email,
+ organization: {
+ orgId: props.organization,
+ },
+ profile: {
+ givenName: props.firstName,
+ familyName: props.lastName,
+ },
+ email: {
+ email: props.email,
+ isVerified: true,
+ },
+ phone: {
+ phone: props.phone,
+ isVerified: true,
+ },
+ password: {
+ password: props.password,
+ changeRequired: props.passwordChangeRequired ?? false,
+ },
+ };
+ if (!props.isEmailVerified) {
+ delete body.email.isVerified;
+ }
+ if (!props.isPhoneVerified) {
+ delete body.phone.isVerified;
+ }
+
+ return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body);
+}
+
+export async function removeUserByUsername(username: string) {
+ const resp = await getUserByUsername(username);
+ if (!resp || !resp.result || !resp.result[0]) {
+ return;
+ }
+ await removeUser(resp.result[0].userId);
+}
+
+export async function removeUser(id: string) {
+ await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`);
+}
+
+async function deleteCall(url: string) {
+ try {
+ const response = await axios.delete(url, {
+ headers: {
+ Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
+ },
+ });
+
+ if (response.status >= 400 && response.status !== 404) {
+ const error = `HTTP Error: ${response.status} - ${response.statusText}`;
+ console.error(error);
+ throw new Error(error);
+ }
+ } catch (error) {
+ console.error("Error making request:", error);
+ throw error;
+ }
+}
+
+export async function getUserByUsername(username: string): Promise {
+ 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_ADMIN_TOKEN}`,
+ },
+ });
+
+ if (response.status >= 400) {
+ const error = `HTTP Error: ${response.status} - ${response.statusText}`;
+ console.error(error);
+ throw new Error(error);
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error("Error making request:", error);
+ throw error;
+ }
+}
+
+export async function activateOTP(userId: string, type: OtpType) {
+ let url = "otp_";
+ switch (type) {
+ case OtpType.sms:
+ url = url + "sms";
+ break;
+ case OtpType.email:
+ url = url + "email";
+ break;
+ }
+
+ await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {});
+}
+
+async function pushCall(url: string, data: any) {
+ try {
+ const response = await axios.post(url, data, {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
+ },
+ });
+
+ if (response.status >= 400) {
+ const error = `HTTP Error: ${response.status} - ${response.statusText}`;
+ console.error(error);
+ throw new Error(error);
+ }
+ } catch (error) {
+ console.error("Error making request:", error);
+ throw error;
+ }
+}
+
+export async function addTOTP(userId: string): Promise {
+ const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {});
+ const code = totp(response.secret);
+ await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code });
+ return response.secret;
+}
+
+export function totp(secret: string) {
+ const authenticator = new Authenticator({
+ createDigest,
+ createRandomBytes,
+ keyDecoder,
+ keyEncoder,
+ });
+ // google authenticator usage
+ const token = authenticator.generate(secret);
+
+ // check if token can be used
+ if (!authenticator.verify({ token: token, secret: secret })) {
+ const error = `Generated token could not be verified`;
+ console.error(error);
+ throw new Error(error);
+ }
+
+ return token;
+}
+
+export async function eventualNewUser(id: string) {
+ return request({
+ url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`,
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
+ "Content-Type": "application/json",
+ },
+ retryConfig: {
+ statusCodesToRetry: [[404, 404]],
+ retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
+ totalTimeout: 10000, // 10 seconds
+ onRetryAttempt: (error) => {
+ console.warn(`Retrying to query new user ${id}: ${error.message}`);
+ },
+ },
+ });
+}
diff --git a/apps/login/acceptance/zitadel.yaml b/apps/login/acceptance/zitadel.yaml
new file mode 100644
index 0000000000..986574fed2
--- /dev/null
+++ b/apps/login/acceptance/zitadel.yaml
@@ -0,0 +1,65 @@
+ExternalSecure: false
+TLS.Enabled: false
+
+FirstInstance:
+ 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
+ Pat.ExpirationDate: 2099-01-01T00:00:00Z
+ LoginClient:
+ Machine:
+ Username: login-client-sa
+ Name: Login Client
+ Pat.ExpirationDate: 2099-01-01T00:00:00Z
+
+DefaultInstance:
+ LoginPolicy:
+ AllowUsernamePassword: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD
+ AllowRegister: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER
+ AllowExternalIDP: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP
+ ForceMFA: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA
+ HidePasswordReset: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET
+ IgnoreUnknownUsernames: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_IGNOREUNKNOWNUSERNAMES
+ AllowDomainDiscovery: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY
+ # 1 is allowed, 0 is not allowed
+ PasswordlessType: 1 # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE
+ # DefaultRedirectURL is empty by default because we use the Console UI
+ DefaultRedirectURI: # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_DEFAULTREDIRECTURI
+ # 240h = 10d
+ PasswordCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDCHECKLIFETIME
+ # 240h = 10d
+ ExternalLoginCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_EXTERNALLOGINCHECKLIFETIME
+ # 720h = 30d
+ MfaInitSkipLifetime: 0h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME
+ SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME
+ MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME
+ 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:
+ EventPushConnRatio: 0.2 # 4
+ ProjectionSpoolerConnRatio: 0.3 # 6
+ postgres:
+ Host: db
+ MaxOpenConns: 20
+ MaxIdleConns: 20
+ MaxConnLifetime: 1h
+ MaxConnIdleTime: 5m
+ User.Password: zitadel
+
+Logstore.Access.Stdout.Enabled: true
+Log.Formatter.Format: json
\ No newline at end of file
diff --git a/apps/login/constants/csp.js b/apps/login/constants/csp.js
new file mode 100644
index 0000000000..5cc1e254f3
--- /dev/null
+++ b/apps/login/constants/csp.js
@@ -0,0 +1,2 @@
+export const DEFAULT_CSP =
+ "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;";
diff --git a/apps/login/docker-bake-release.hcl b/apps/login/docker-bake-release.hcl
new file mode 100644
index 0000000000..51e1c194f6
--- /dev/null
+++ b/apps/login/docker-bake-release.hcl
@@ -0,0 +1,3 @@
+target "release" {
+ platforms = ["linux/amd64", "linux/arm64"]
+}
diff --git a/apps/login/docker-bake.hcl b/apps/login/docker-bake.hcl
new file mode 100644
index 0000000000..e09d1176e0
--- /dev/null
+++ b/apps/login/docker-bake.hcl
@@ -0,0 +1,25 @@
+variable "LOGIN_TAG" {
+ default = "zitadel-login:local"
+}
+
+group "default" {
+ targets = ["login-standalone"]
+}
+
+# The release target is overwritten in docker-bake-release.hcl
+# It makes sure the image is built for multiple platforms.
+# By default the platforms property is empty, so images are only built for the current bake runtime platform.
+target "release" {}
+
+target "docker-metadata-action" {
+ # In the pipeline, this target is overwritten by the docker metadata action.
+ tags = ["${LOGIN_TAG}"]
+}
+
+# We run integration and acceptance tests against the next standalone server for docker.
+target "login-standalone" {
+ inherits = [
+ "docker-metadata-action",
+ "release",
+ ]
+}
diff --git a/apps/login/integration/.eslintrc.cjs b/apps/login/integration/.eslintrc.cjs
new file mode 100644
index 0000000000..84711c5881
--- /dev/null
+++ b/apps/login/integration/.eslintrc.cjs
@@ -0,0 +1,10 @@
+module.exports = {
+ root: true,
+ // Use basic ESLint config since the login app has its own detailed config
+ extends: ["eslint:recommended"],
+ settings: {
+ next: {
+ rootDir: ["apps/*/"],
+ },
+ },
+};
diff --git a/apps/login/integration/.gitignore b/apps/login/integration/.gitignore
new file mode 100644
index 0000000000..2ca81ab137
--- /dev/null
+++ b/apps/login/integration/.gitignore
@@ -0,0 +1,2 @@
+screenshots
+videos
\ No newline at end of file
diff --git a/apps/login/integration/.npmrc b/apps/login/integration/.npmrc
new file mode 100644
index 0000000000..bc63bba6e3
--- /dev/null
+++ b/apps/login/integration/.npmrc
@@ -0,0 +1 @@
+side-effects-cache=false
diff --git a/apps/login/integration/core-mock/Dockerfile b/apps/login/integration/core-mock/Dockerfile
new file mode 100644
index 0000000000..447c73b534
--- /dev/null
+++ b/apps/login/integration/core-mock/Dockerfile
@@ -0,0 +1,15 @@
+FROM bufbuild/buf:1.54.0 AS proto-files
+RUN buf export https://github.com/envoyproxy/protoc-gen-validate.git --path validate --output /proto-files && \
+ buf export https://github.com/grpc-ecosystem/grpc-gateway.git --path protoc-gen-openapiv2 --output /proto-files && \
+ buf export https://github.com/googleapis/googleapis.git --path protos/zitadelgoogle/api/annotations.proto --path google/api/http.proto --path google/api/field_behavior.proto --output /proto-files && \
+ buf export https://github.com/zitadel/zitadel.git --path ./proto/zitadel --output /proto-files
+
+FROM golang:1.20.5-alpine3.18 AS mock-zitadel
+
+RUN go install github.com/eliobischof/grpc-mock/cmd/grpc-mock@01b09f60db1b501178af59bed03b2c22661df48c
+
+COPY mocked-services.cfg .
+COPY initial-stubs initial-stubs
+COPY --from=proto-files /proto-files/ ./
+
+ENTRYPOINT [ "sh", "-c", "grpc-mock -v 1 -proto $(tr '\n' ',' < ./mocked-services.cfg) -stub-dir ./initial-stubs -mock-addr :22222" ]
diff --git a/apps/login/integration/core-mock/initial-stubs/zitadel.settings.v2.SettingsService.json b/apps/login/integration/core-mock/initial-stubs/zitadel.settings.v2.SettingsService.json
new file mode 100644
index 0000000000..ebfaaadb85
--- /dev/null
+++ b/apps/login/integration/core-mock/initial-stubs/zitadel.settings.v2.SettingsService.json
@@ -0,0 +1,66 @@
+[
+ {
+ "service": "zitadel.settings.v2.SettingsService",
+ "method": "GetBrandingSettings",
+ "out": {
+ "data": {}
+ }
+ },
+ {
+ "service": "zitadel.settings.v2.SettingsService",
+ "method": "GetSecuritySettings",
+ "out": {
+ "data": {}
+ }
+ },
+ {
+ "service": "zitadel.settings.v2.SettingsService",
+ "method": "GetLegalAndSupportSettings",
+ "out": {
+ "data": {
+ "settings": {
+ "tosLink": "http://whatever.com/help",
+ "privacyPolicyLink": "http://whatever.com/help",
+ "helpLink": "http://whatever.com/help"
+ }
+ }
+ }
+ },
+ {
+ "service": "zitadel.settings.v2.SettingsService",
+ "method": "GetActiveIdentityProviders",
+ "out": {
+ "data": {
+ "identityProviders": [
+ {
+ "id": "123",
+ "name": "Hubba bubba",
+ "type": 10
+ }
+ ]
+ }
+ }
+ },
+ {
+ "service": "zitadel.settings.v2.SettingsService",
+ "method": "GetPasswordComplexitySettings",
+ "out": {
+ "data": {
+ "settings": {
+ "minLength": 8,
+ "requiresUppercase": true,
+ "requiresLowercase": true,
+ "requiresNumber": true,
+ "requiresSymbol": true
+ }
+ }
+ }
+ },
+ {
+ "service": "zitadel.settings.v2.SettingsService",
+ "method": "GetHostedLoginTranslation",
+ "out": {
+ "data": {}
+ }
+ }
+]
diff --git a/apps/login/integration/core-mock/mocked-services.cfg b/apps/login/integration/core-mock/mocked-services.cfg
new file mode 100644
index 0000000000..6a758ab8c1
--- /dev/null
+++ b/apps/login/integration/core-mock/mocked-services.cfg
@@ -0,0 +1,7 @@
+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
+zitadel/auth.proto
+zitadel/admin.proto
\ No newline at end of file
diff --git a/apps/login/integration/cypress.config.ts b/apps/login/integration/cypress.config.ts
new file mode 100644
index 0000000000..a115cd9d1a
--- /dev/null
+++ b/apps/login/integration/cypress.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "cypress";
+
+export default defineConfig({
+ reporter: "list",
+
+ e2e: {
+ baseUrl: process.env.LOGIN_BASE_URL || "http://localhost:3001/ui/v2/login",
+ specPattern: "integration/**/*.cy.{js,jsx,ts,tsx}",
+ supportFile: "support/e2e.{js,jsx,ts,tsx}",
+ setupNodeEvents(on, config) {
+ // implement node event listeners here
+ },
+ },
+});
diff --git a/apps/login/integration/fixtures/example.json b/apps/login/integration/fixtures/example.json
new file mode 100644
index 0000000000..02e4254378
--- /dev/null
+++ b/apps/login/integration/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
diff --git a/apps/login/integration/integration/invite.cy.ts b/apps/login/integration/integration/invite.cy.ts
new file mode 100644
index 0000000000..a68ff96c36
--- /dev/null
+++ b/apps/login/integration/integration/invite.cy.ts
@@ -0,0 +1,110 @@
+import { stub } from "../support/e2e";
+
+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.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/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/integration/integration/login.cy.ts b/apps/login/integration/integration/login.cy.ts
new file mode 100644
index 0000000000..917d719cb1
--- /dev/null
+++ b/apps/login/integration/integration/login.cy.ts
@@ -0,0 +1,172 @@
+import { stub } from "../support/e2e";
+
+describe("login", () => {
+ beforeEach(() => {
+ stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [{ id: "256088834543534543" }],
+ },
+ });
+ 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,
+ },
+ },
+ });
+ });
+ describe("password login", () => {
+ beforeEach(() => {
+ stub("zitadel.user.v2.UserService", "ListUsers", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [
+ {
+ 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: true,
+ },
+ },
+ },
+ ],
+ },
+ });
+ stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
+ data: {
+ authMethodTypes: [1], // 1 for password authentication
+ },
+ });
+ });
+ it("should redirect a user with password authentication to /password", () => {
+ cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
+ cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/password");
+ });
+ describe("with passkey prompt", () => {
+ beforeEach(() => {
+ stub("zitadel.session.v2.SessionService", "SetSession", {
+ data: {
+ details: {
+ sequence: 859,
+ changeDate: "2023-07-04T07:58:20.126Z",
+ resourceOwner: "220516472055706145",
+ },
+ sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
+ challenges: undefined,
+ },
+ });
+ });
+ // 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", () => {
+ beforeEach(() => {
+ stub("zitadel.user.v2.UserService", "ListUsers", {
+ data: {
+ details: {
+ totalResult: 1,
+ },
+ result: [
+ {
+ 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: true,
+ },
+ },
+ },
+ ],
+ },
+ });
+ stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
+ data: {
+ authMethodTypes: [2], // 2 for passwordless authentication
+ },
+ });
+ });
+
+ it("should redirect a user with passwordless authentication to /passkey", () => {
+ cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
+ cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey");
+ });
+ });
+});
diff --git a/apps/login/integration/integration/register-idp.cy.ts b/apps/login/integration/integration/register-idp.cy.ts
new file mode 100644
index 0000000000..73a0c32e00
--- /dev/null
+++ b/apps/login/integration/integration/register-idp.cy.ts
@@ -0,0 +1,21 @@
+import { stub } from "../support/e2e";
+
+const IDP_URL = "https://example.com/idp/url";
+
+describe("register idps", () => {
+ beforeEach(() => {
+ stub("zitadel.user.v2.UserService", "StartIdentityProviderIntent", {
+ data: {
+ authUrl: IDP_URL,
+ },
+ });
+ });
+
+ it("should redirect the user to the correct url", () => {
+ cy.visit("/idp");
+ cy.get('button[e2e="google"]').click();
+ cy.origin(IDP_URL, { args: IDP_URL }, (url) => {
+ cy.location("href", { timeout: 10_000 }).should("eq", url);
+ });
+ });
+});
diff --git a/apps/login/integration/integration/register.cy.ts b/apps/login/integration/integration/register.cy.ts
new file mode 100644
index 0000000000..44c53647c1
--- /dev/null
+++ b/apps/login/integration/integration/register.cy.ts
@@ -0,0 +1,73 @@
+import { stub } from "../support/e2e";
+
+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: "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 /passkey/set", () => {
+ cy.visit("/register");
+ 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.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey/set");
+ });
+});
diff --git a/apps/login/integration/integration/verify.cy.ts b/apps/login/integration/integration/verify.cy.ts
new file mode 100644
index 0000000000..db80cea720
--- /dev/null
+++ b/apps/login/integration/integration/verify.cy.ts
@@ -0,0 +1,95 @@
+import { stub } from "../support/e2e";
+
+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 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=221394658884845598&code=abc");
+ cy.contains("Could not verify email", { timeout: 10_000 });
+ });
+});
diff --git a/apps/login/integration/support/e2e.ts b/apps/login/integration/support/e2e.ts
new file mode 100644
index 0000000000..1ac0eb3948
--- /dev/null
+++ b/apps/login/integration/support/e2e.ts
@@ -0,0 +1,29 @@
+const url = Cypress.env("CORE_MOCK_STUBS_URL") || "http://mock-zitadel:22220/v1/stubs";
+
+function removeStub(service: string, method: string) {
+ return cy.request({
+ url,
+ method: "DELETE",
+ qs: {
+ service,
+ method,
+ },
+ });
+}
+
+export function stub(service: string, method: string, out?: any) {
+ removeStub(service, method);
+ return cy.request({
+ url,
+ method: "POST",
+ body: {
+ stubs: [
+ {
+ service,
+ method,
+ out,
+ },
+ ],
+ },
+ });
+}
diff --git a/apps/login/integration/tsconfig.json b/apps/login/integration/tsconfig.json
new file mode 100644
index 0000000000..18edb199ac
--- /dev/null
+++ b/apps/login/integration/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["es5", "dom"],
+ "types": ["cypress", "node"]
+ },
+ "include": ["**/*.ts"]
+}
diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json
new file mode 100644
index 0000000000..7b2a507fe4
--- /dev/null
+++ b/apps/login/locales/de.json
@@ -0,0 +1,292 @@
+{
+ "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",
+ "verified": "verifiziert",
+ "expired": "abgelaufen"
+ },
+ "logout": {
+ "title": "Logout",
+ "description": "Wählen Sie den Account aus, das Sie entfernen möchten",
+ "noResults": "Keine Konten gefunden",
+ "clear": "Session beenden",
+ "verifiedAt": "Zuletzt aktiv: {time}",
+ "success": {
+ "title": "Logout erfolgreich",
+ "description": "Sie haben sich erfolgreich abgemeldet."
+ }
+ },
+ "loginname": {
+ "title": "Willkommen zurück!",
+ "description": "Geben Sie Ihre Anmeldedaten ein.",
+ "register": "Neuen Benutzer registrieren",
+ "submit": "Weiter",
+ "required": {
+ "loginName": "Dieses Feld ist erforderlich"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "Passwort",
+ "description": "Geben Sie Ihr Passwort ein.",
+ "resetPassword": "Passwort zurücksetzen",
+ "submit": "Weiter",
+ "required": {
+ "password": "Dieses Feld ist erforderlich"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Dieses Feld ist erforderlich",
+ "newPassword": "Bitte geben Sie ein Passwort ein!",
+ "confirmPassword": "Dieses Feld ist erforderlich"
+ }
+ },
+ "change": {
+ "title": "Passwort ändern",
+ "description": "Legen Sie das Passwort für Ihr Konto fest",
+ "submit": "Weiter",
+ "required": {
+ "newPassword": "Bitte geben Sie ein neues Passwort ein!",
+ "confirmPassword": "Dieses Feld ist erforderlich"
+ }
+ }
+ },
+ "idp": {
+ "title": "Mit SSO anmelden",
+ "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden",
+ "orSignInWith": "oder melden Sie sich an mit",
+ "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."
+ },
+ "completeRegister": {
+ "title": "Registrierung abschließen",
+ "description": "Bitte vervollständige die Registrierung, um dein Konto zu erstellen."
+ }
+ },
+ "ldap": {
+ "title": "LDAP Login",
+ "description": "Geben Sie Ihre LDAP-Anmeldedaten ein.",
+ "username": "Benutzername",
+ "password": "Passwort",
+ "submit": "Weiter",
+ "required": {
+ "username": "Dieses Feld ist erforderlich",
+ "password": "Dieses Feld ist erforderlich"
+ }
+ },
+ "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.",
+ "skip": "Überspringen"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Dieses Feld ist erforderlich"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Dieses Feld ist erforderlich"
+ }
+ }
+ },
+ "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.",
+ "noMethodAvailableWarning": "Keine Authentifizierungsmethode verfügbar. Bitte wenden Sie sich an den Administrator.",
+ "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",
+ "orUseIDP": "oder verwenden Sie einen Identitätsanbieter",
+ "password": {
+ "title": "Passwort festlegen",
+ "description": "Legen Sie das Passwort für Ihr Konto fest",
+ "submit": "Weiter",
+ "required": {
+ "password": "Bitte geben Sie ein Passwort ein!",
+ "confirmPassword": "Dieses Feld ist erforderlich"
+ }
+ },
+ "required": {
+ "firstname": "Dieses Feld ist erforderlich",
+ "lastname": "Dieses Feld ist erforderlich",
+ "email": "Dieses Feld ist erforderlich"
+ }
+ },
+ "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",
+ "error": {
+ "title": "Fehler",
+ "description": "Ein Fehler ist aufgetreten."
+ }
+ },
+ "verify": {
+ "userIdMissing": "Keine Benutzer-ID angegeben!",
+ "successTitle": "Benutzer verifiziert",
+ "successDescription": "Der Benutzer wurde 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",
+ "codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.",
+ "submit": "Weiter",
+ "required": {
+ "code": "Dieses Feld ist erforderlich"
+ }
+ }
+ },
+ "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"
+ },
+ "device": {
+ "usercode": {
+ "title": "Gerätecode",
+ "description": "Geben Sie den Code ein.",
+ "submit": "Weiter",
+ "required": {
+ "code": "Dieses Feld ist erforderlich"
+ }
+ },
+ "request": {
+ "title": "{appName} möchte eine Verbindung herstellen:",
+ "disclaimer": "{appName} hat Zugriff auf:",
+ "description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.",
+ "submit": "Zulassen",
+ "deny": "Ablehnen"
+ },
+ "scope": {
+ "openid": "Überprüfen Ihrer Identität.",
+ "email": "Zugriff auf Ihre E-Mail-Adresse.",
+ "profile": "Zugriff auf Ihre vollständigen Profilinformationen.",
+ "offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto."
+ }
+ },
+ "error": {
+ "noUserCode": "Kein Benutzercode angegeben!",
+ "noDeviceRequest": " Es wurde keine Geräteanforderung gefunden. Bitte überprüfen Sie die URL.",
+ "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..e1b7e4e82c
--- /dev/null
+++ b/apps/login/locales/en.json
@@ -0,0 +1,292 @@
+{
+ "common": {
+ "back": "Back"
+ },
+ "accounts": {
+ "title": "Accounts",
+ "description": "Select the account you want to use.",
+ "addAnother": "Add another account",
+ "noResults": "No accounts found",
+ "verified": "verified",
+ "expired": "expired"
+ },
+ "logout": {
+ "title": "Logout",
+ "description": "Click an account to end the session",
+ "noResults": "No accounts found",
+ "clear": "End Session",
+ "verifiedAt": "Last active: {time}",
+ "success": {
+ "title": "Logout successful",
+ "description": "You have successfully logged out."
+ }
+ },
+ "loginname": {
+ "title": "Welcome back!",
+ "description": "Enter your login data.",
+ "register": "Register new user",
+ "submit": "Continue",
+ "required": {
+ "loginName": "This field is required"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "Password",
+ "description": "Enter your password.",
+ "resetPassword": "Reset Password",
+ "submit": "Continue",
+ "required": {
+ "password": "This field is required"
+ }
+ },
+ "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",
+ "required": {
+ "code": "This field is required",
+ "newPassword": "You have to provide a password!",
+ "confirmPassword": "This field is required"
+ }
+ },
+ "change": {
+ "title": "Change Password",
+ "description": "Set the password for your account",
+ "submit": "Continue",
+ "required": {
+ "newPassword": "You have to provide a new password!",
+ "confirmPassword": "This field is required"
+ }
+ }
+ },
+ "idp": {
+ "title": "Sign in with SSO",
+ "description": "Select one of the following providers to sign in",
+ "orSignInWith": "or sign in with",
+ "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."
+ },
+ "completeRegister": {
+ "title": "Complete your data",
+ "description": "You need to complete your registration by providing your email address and name."
+ }
+ },
+ "ldap": {
+ "title": "LDAP Login",
+ "description": "Enter your LDAP credentials.",
+ "username": "Username",
+ "password": "Password",
+ "submit": "Continue",
+ "required": {
+ "username": "This field is required",
+ "password": "This field is required"
+ }
+ },
+ "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.",
+ "skip": "Skip"
+ }
+ },
+ "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",
+ "required": {
+ "code": "This field is required"
+ }
+ },
+ "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",
+ "required": {
+ "code": "This field is required"
+ }
+ }
+ },
+ "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.",
+ "noMethodAvailableWarning": "No authentication method available. Please contact your administrator.",
+ "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",
+ "orUseIDP": "or use an Identity Provider",
+ "password": {
+ "title": "Set Password",
+ "description": "Set the password for your account",
+ "submit": "Continue",
+ "required": {
+ "password": "You have to provide a password!",
+ "confirmPassword": "This field is required"
+ }
+ },
+ "required": {
+ "firstname": "This field is required",
+ "lastname": "This field is required",
+ "email": "This field is required"
+ }
+ },
+ "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",
+ "error": {
+ "title": "Error",
+ "description": "An error occurred while trying to sign in."
+ }
+ },
+ "verify": {
+ "userIdMissing": "No userId provided!",
+ "successTitle": "User verified",
+ "successDescription": "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",
+ "codeSent": "A code has just been sent to your email address.",
+ "submit": "Continue",
+ "required": {
+ "code": "This field is required"
+ }
+ }
+ },
+ "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"
+ },
+ "device": {
+ "usercode": {
+ "title": "Device code",
+ "description": "Enter the code displayed on your app or device.",
+ "submit": "Continue",
+ "required": {
+ "code": "This field is required"
+ }
+ },
+ "request": {
+ "title": "{appName} would like to connect",
+ "description": "{appName} will have access to:",
+ "disclaimer": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.",
+ "submit": "Allow",
+ "deny": "Deny"
+ },
+ "scope": {
+ "openid": "Verify your identity.",
+ "email": "View your email address.",
+ "profile": "View your full profile information.",
+ "offline_access": "Allow offline access to your account."
+ }
+ },
+ "error": {
+ "noUserCode": "No user code provided!",
+ "noDeviceRequest": "No device request found.",
+ "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..b1c63583a7
--- /dev/null
+++ b/apps/login/locales/es.json
@@ -0,0 +1,292 @@
+{
+ "common": {
+ "back": "Atrás"
+ },
+ "accounts": {
+ "title": "Cuentas",
+ "description": "Seleccione la cuenta que desea utilizar.",
+ "addAnother": "Agregar otra cuenta",
+ "noResults": "No se encontraron cuentas",
+ "verified": "verificado",
+ "expired": "expirado"
+ },
+ "logout": {
+ "title": "Cerrar sesión",
+ "description": "Selecciona la cuenta que deseas eliminar",
+ "noResults": "No se encontraron cuentas",
+ "clear": "Eliminar sesión",
+ "verifiedAt": "Última actividad: {time}",
+ "success": {
+ "title": "Cierre de sesión exitoso",
+ "description": "Has cerrado sesión correctamente."
+ }
+ },
+ "loginname": {
+ "title": "¡Bienvenido de nuevo!",
+ "description": "Introduce tus datos de acceso.",
+ "register": "Registrar nuevo usuario",
+ "submit": "Continuar",
+ "required": {
+ "loginName": "Este campo es obligatorio"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "Contraseña",
+ "description": "Introduce tu contraseña.",
+ "resetPassword": "Restablecer contraseña",
+ "submit": "Continuar",
+ "required": {
+ "password": "Este campo es obligatorio"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Este campo es obligatorio",
+ "newPassword": "¡Debes proporcionar una contraseña!",
+ "confirmPassword": "Este campo es obligatorio"
+ }
+ },
+ "change": {
+ "title": "Cambiar Contraseña",
+ "description": "Establece la contraseña para tu cuenta",
+ "submit": "Continuar",
+ "required": {
+ "newPassword": "¡Debes proporcionar una nueva contraseña!",
+ "confirmPassword": "Este campo es obligatorio"
+ }
+ }
+ },
+ "idp": {
+ "title": "Iniciar sesión con SSO",
+ "description": "Selecciona uno de los siguientes proveedores para iniciar sesión",
+ "orSignInWith": "o iniciar sesión con",
+ "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."
+ },
+ "completeRegister": {
+ "title": "Completar registro",
+ "description": "Para completar el registro, debes establecer una contraseña."
+ }
+ },
+ "ldap": {
+ "title": "Iniciar sesión con LDAP",
+ "description": "Introduce tus credenciales LDAP.",
+ "username": "Nombre de usuario",
+ "password": "Contraseña",
+ "submit": "Continuar",
+ "required": {
+ "username": "Este campo es obligatorio",
+ "password": "Este campo es obligatorio"
+ }
+ },
+ "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.",
+ "skip": "Omitir"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Este campo es obligatorio"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Este campo es obligatorio"
+ }
+ }
+ },
+ "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.",
+ "noMethodAvailableWarning": "No hay métodos de autenticación disponibles. Por favor, contacta a tu administrador.",
+ "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",
+ "orUseIDP": "o usa un Proveedor de Identidad",
+ "password": {
+ "title": "Establecer Contraseña",
+ "description": "Establece la contraseña para tu cuenta",
+ "submit": "Continuar",
+ "required": {
+ "password": "¡Debes proporcionar una contraseña!",
+ "confirmPassword": "Este campo es obligatorio"
+ }
+ },
+ "required": {
+ "firstname": "Este campo es obligatorio",
+ "lastname": "Este campo es obligatorio",
+ "email": "Este campo es obligatorio"
+ }
+ },
+ "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",
+ "error": {
+ "title": "Error",
+ "description": "Ocurrió un error al iniciar sesión."
+ }
+ },
+ "verify": {
+ "userIdMissing": "¡No se proporcionó userId!",
+ "successTitle": "Usuario verificado",
+ "successDescription": "El usuario ha sido verificado con éxito.",
+ "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",
+ "codeSent": "Se ha enviado un código a tu dirección de correo electrónico.",
+ "submit": "Continuar",
+ "required": {
+ "code": "Este campo es obligatorio"
+ }
+ }
+ },
+ "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"
+ },
+ "device": {
+ "usercode": {
+ "title": "Código del dispositivo",
+ "description": "Introduce el código.",
+ "submit": "Continuar",
+ "required": {
+ "code": "Este campo es obligatorio"
+ }
+ },
+ "request": {
+ "title": "{appName} desea conectarse:",
+ "description": "{appName} tendrá acceso a:",
+ "disclaimer": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.",
+ "submit": "Permitir",
+ "deny": "Denegar"
+ },
+ "scope": {
+ "openid": "Verifica tu identidad.",
+ "email": "Accede a tu dirección de correo electrónico.",
+ "profile": "Accede a la información completa de tu perfil.",
+ "offline_access": "Permitir acceso sin conexión a tu cuenta."
+ }
+ },
+ "error": {
+ "noUserCode": "¡No se proporcionó código de usuario!",
+ "noDeviceRequest": "No se encontró ninguna solicitud de dispositivo.",
+ "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..a71b48ed04
--- /dev/null
+++ b/apps/login/locales/it.json
@@ -0,0 +1,292 @@
+{
+ "common": {
+ "back": "Indietro"
+ },
+ "accounts": {
+ "title": "Account",
+ "description": "Seleziona l'account che vuoi utilizzare.",
+ "addAnother": "Aggiungi un altro account",
+ "noResults": "Nessun account trovato",
+ "verified": "verificato",
+ "expired": "scaduto"
+ },
+ "logout": {
+ "title": "Esci",
+ "description": "Seleziona l'account che desideri uscire",
+ "noResults": "Nessun account trovato",
+ "clear": "Elimina sessione",
+ "verifiedAt": "Ultima attività: {time}",
+ "success": {
+ "title": "Uscita riuscita",
+ "description": "Hai effettuato l'uscita con successo."
+ }
+ },
+ "loginname": {
+ "title": "Bentornato!",
+ "description": "Inserisci i tuoi dati di accesso.",
+ "register": "Registrati come nuovo utente",
+ "submit": "Continua",
+ "required": {
+ "loginName": "Questo campo è obbligatorio"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "Password",
+ "description": "Inserisci la tua password.",
+ "resetPassword": "Reimposta Password",
+ "submit": "Continua",
+ "required": {
+ "password": "Questo campo è obbligatorio"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Questo campo è obbligatorio",
+ "newPassword": "Devi fornire una password!",
+ "confirmPassword": "Questo campo è obbligatorio"
+ }
+ },
+ "change": {
+ "title": "Cambia Password",
+ "description": "Imposta la password per il tuo account",
+ "submit": "Continua",
+ "required": {
+ "newPassword": "Devi fornire una nuova password!",
+ "confirmPassword": "Questo campo è obbligatorio"
+ }
+ }
+ },
+ "idp": {
+ "title": "Accedi con SSO",
+ "description": "Seleziona uno dei seguenti provider per accedere",
+ "orSignInWith": "o accedi con",
+ "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."
+ },
+ "completeRegister": {
+ "title": "Completa la registrazione",
+ "description": "Completa la registrazione del tuo account."
+ }
+ },
+ "ldap": {
+ "title": "Accedi con LDAP",
+ "description": "Inserisci le tue credenziali LDAP.",
+ "username": "Nome utente",
+ "password": "Password",
+ "submit": "Continua",
+ "required": {
+ "username": "Questo campo è obbligatorio",
+ "password": "Questo campo è obbligatorio"
+ }
+ },
+ "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.",
+ "skip": "Salta"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Questo campo è obbligatorio"
+ }
+ },
+ "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",
+ "required": {
+ "code": "Questo campo è obbligatorio"
+ }
+ }
+ },
+ "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.",
+ "noMethodAvailableWarning": "Nessun metodo di autenticazione disponibile. Contatta l'amministratore di sistema per assistenza.",
+ "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",
+ "orUseIDP": "o usa un Identity Provider",
+ "password": {
+ "title": "Imposta Password",
+ "description": "Imposta la password per il tuo account",
+ "submit": "Continua",
+ "required": {
+ "password": "Devi fornire una password!",
+ "confirmPassword": "Questo campo è obbligatorio"
+ }
+ },
+ "required": {
+ "firstname": "Questo campo è obbligatorio",
+ "lastname": "Questo campo è obbligatorio",
+ "email": "Questo campo è obbligatorio"
+ }
+ },
+ "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",
+ "error": {
+ "title": "Errore",
+ "description": "Si è verificato un errore durante il tentativo di accesso."
+ }
+ },
+ "verify": {
+ "userIdMissing": "Nessun userId fornito!",
+ "successTitle": "Utente verificato",
+ "successDescription": "L'utente è stato verificato 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",
+ "codeSent": "Un codice è stato appena inviato al tuo indirizzo email.",
+ "submit": "Continua",
+ "required": {
+ "code": "Questo campo è obbligatorio"
+ }
+ }
+ },
+ "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"
+ },
+ "device": {
+ "usercode": {
+ "title": "Codice dispositivo",
+ "description": "Inserisci il codice.",
+ "submit": "Continua",
+ "required": {
+ "code": "Questo campo è obbligatorio"
+ }
+ },
+ "request": {
+ "title": "{appName} desidera connettersi:",
+ "description": "{appName} avrà accesso a:",
+ "disclaimer": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.",
+ "submit": "Consenti",
+ "deny": "Nega"
+ },
+ "scope": {
+ "openid": "Verifica la tua identità.",
+ "email": "Accedi al tuo indirizzo email.",
+ "profile": "Accedi alle informazioni complete del tuo profilo.",
+ "offline_access": "Consenti l'accesso offline al tuo account."
+ }
+ },
+ "error": {
+ "noUserCode": "Nessun codice utente fornito!",
+ "noDeviceRequest": "Nessuna richiesta di dispositivo trovata.",
+ "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/pl.json b/apps/login/locales/pl.json
new file mode 100644
index 0000000000..3e8562a220
--- /dev/null
+++ b/apps/login/locales/pl.json
@@ -0,0 +1,292 @@
+{
+ "common": {
+ "back": "Powrót"
+ },
+ "accounts": {
+ "title": "Konta",
+ "description": "Wybierz konto, którego chcesz użyć.",
+ "addAnother": "Dodaj kolejne konto",
+ "noResults": "Nie znaleziono kont",
+ "verified": "zweryfikowany",
+ "expired": "wygasł"
+ },
+ "logout": {
+ "title": "Wyloguj się",
+ "description": "Wybierz konto, które chcesz usunąć",
+ "noResults": "Nie znaleziono kont",
+ "clear": "Usuń sesję",
+ "verifiedAt": "Ostatnia aktywność: {time}",
+ "success": {
+ "title": "Wylogowanie udane",
+ "description": "Pomyślnie się wylogowałeś."
+ }
+ },
+ "loginname": {
+ "title": "Witamy ponownie!",
+ "description": "Wprowadź dane logowania.",
+ "register": "Zarejestruj nowego użytkownika",
+ "submit": "Kontynuuj",
+ "required": {
+ "loginName": "To pole jest wymagane"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "Hasło",
+ "description": "Wprowadź swoje hasło.",
+ "resetPassword": "Zresetuj hasło",
+ "submit": "Kontynuuj",
+ "required": {
+ "password": "To pole jest wymagane"
+ }
+ },
+ "set": {
+ "title": "Ustaw hasło",
+ "description": "Ustaw hasło dla swojego konta",
+ "codeSent": "Kod został wysłany na twój adres e-mail.",
+ "noCodeReceived": "Nie otrzymałeś kodu?",
+ "resend": "Wyślij kod ponownie",
+ "submit": "Kontynuuj",
+ "required": {
+ "code": "To pole jest wymagane",
+ "newPassword": "Musisz podać hasło!",
+ "confirmPassword": "To pole jest wymagane"
+ }
+ },
+ "change": {
+ "title": "Zmień hasło",
+ "description": "Ustaw nowe hasło dla swojego konta",
+ "submit": "Kontynuuj",
+ "required": {
+ "newPassword": "Musisz podać nowe hasło!",
+ "confirmPassword": "To pole jest wymagane"
+ }
+ }
+ },
+ "idp": {
+ "title": "Zaloguj się za pomocą SSO",
+ "description": "Wybierz jednego z poniższych dostawców, aby się zalogować",
+ "orSignInWith": "lub zaloguj się przez",
+ "signInWithApple": "Zaloguj się przez Apple",
+ "signInWithGoogle": "Zaloguj się przez Google",
+ "signInWithAzureAD": "Zaloguj się przez AzureAD",
+ "signInWithGithub": "Zaloguj się przez GitHub",
+ "signInWithGitlab": "Zaloguj się przez GitLab",
+ "loginSuccess": {
+ "title": "Logowanie udane",
+ "description": "Zostałeś pomyślnie zalogowany!"
+ },
+ "linkingSuccess": {
+ "title": "Konto powiązane",
+ "description": "Pomyślnie powiązałeś swoje konto!"
+ },
+ "registerSuccess": {
+ "title": "Rejestracja udana",
+ "description": "Pomyślnie się zarejestrowałeś!"
+ },
+ "loginError": {
+ "title": "Logowanie nieudane",
+ "description": "Wystąpił błąd podczas próby logowania."
+ },
+ "linkingError": {
+ "title": "Powiązanie konta nie powiodło się",
+ "description": "Wystąpił błąd podczas próby powiązania konta."
+ },
+ "completeRegister": {
+ "title": "Ukończ rejestrację",
+ "description": "Ukończ rejestrację swojego konta."
+ }
+ },
+ "ldap": {
+ "title": "Zaloguj się przez LDAP",
+ "description": "Wprowadź swoje dane logowania LDAP.",
+ "username": "Nazwa użytkownika",
+ "password": "Hasło",
+ "submit": "Kontynuuj",
+ "required": {
+ "username": "To pole jest wymagane",
+ "password": "To pole jest wymagane"
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "Zweryfikuj swoją tożsamość",
+ "description": "Wybierz jeden z poniższych sposobów weryfikacji.",
+ "noResults": "Nie znaleziono dostępnych metod uwierzytelniania dwuskładnikowego."
+ },
+ "set": {
+ "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe",
+ "description": "Wybierz jedną z poniższych metod drugiego czynnika.",
+ "skip": "Pomiń"
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe",
+ "totpDescription": "Wprowadź kod z aplikacji uwierzytelniającej.",
+ "smsDescription": "Wprowadź kod otrzymany SMS-em.",
+ "emailDescription": "Wprowadź kod otrzymany e-mailem.",
+ "noCodeReceived": "Nie otrzymałeś kodu?",
+ "resendCode": "Wyślij kod ponownie",
+ "submit": "Kontynuuj",
+ "required": {
+ "code": "To pole jest wymagane"
+ }
+ },
+ "set": {
+ "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe",
+ "totpDescription": "Zeskanuj kod QR za pomocą aplikacji uwierzytelniającej.",
+ "smsDescription": "Wprowadź swój numer telefonu, aby otrzymać kod SMS-em.",
+ "emailDescription": "Wprowadź swój adres e-mail, aby otrzymać kod e-mailem.",
+ "totpRegisterDescription": "Zeskanuj kod QR lub otwórz adres URL ręcznie.",
+ "submit": "Kontynuuj",
+ "required": {
+ "code": "To pole jest wymagane"
+ }
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "Uwierzytelnij się za pomocą klucza dostępu",
+ "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.",
+ "usePassword": "Użyj hasła",
+ "submit": "Kontynuuj"
+ },
+ "set": {
+ "title": "Skonfiguruj klucz dostępu",
+ "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.",
+ "info": {
+ "description": "Klucz dostępu to metoda uwierzytelniania na urządzeniu, wykorzystująca np. odcisk palca, Apple FaceID lub podobne rozwiązania.",
+ "link": "Uwierzytelnianie bez hasła"
+ },
+ "skip": "Pomiń",
+ "submit": "Kontynuuj"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe",
+ "description": "Zweryfikuj swoje konto za pomocą urządzenia."
+ },
+ "set": {
+ "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe",
+ "description": "Skonfiguruj urządzenie jako dodatkowy czynnik uwierzytelniania.",
+ "submit": "Kontynuuj"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "Klucz dostępu",
+ "password": "Hasło"
+ },
+ "disabled": {
+ "title": "Rejestracja wyłączona",
+ "description": "Rejestracja jest wyłączona. Skontaktuj się z administratorem."
+ },
+ "missingdata": {
+ "title": "Brak danych",
+ "description": "Podaj e-mail, imię i nazwisko, aby się zarejestrować."
+ },
+ "title": "Rejestracja",
+ "description": "Utwórz konto ZITADEL.",
+ "noMethodAvailableWarning": "Brak dostępnych metod uwierzytelniania. Skontaktuj się z administratorem.",
+ "selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć",
+ "agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania",
+ "termsOfService": "Regulamin",
+ "privacyPolicy": "Polityka prywatności",
+ "submit": "Kontynuuj",
+ "orUseIDP": "lub użyj dostawcy tożsamości",
+ "password": {
+ "title": "Ustaw hasło",
+ "description": "Ustaw hasło dla swojego konta",
+ "submit": "Kontynuuj",
+ "required": {
+ "password": "Musisz podać hasło!",
+ "confirmPassword": "To pole jest wymagane"
+ }
+ },
+ "required": {
+ "firstname": "To pole jest wymagane",
+ "lastname": "To pole jest wymagane",
+ "email": "To pole jest wymagane"
+ }
+ },
+ "invite": {
+ "title": "Zaproś użytkownika",
+ "description": "Podaj adres e-mail oraz imię i nazwisko użytkownika, którego chcesz zaprosić.",
+ "info": "Użytkownik otrzyma e-mail z dalszymi instrukcjami.",
+ "notAllowed": "Twoje ustawienia nie pozwalają na zapraszanie użytkowników.",
+ "submit": "Kontynuuj",
+ "success": {
+ "title": "Użytkownik zaproszony",
+ "description": "E-mail został pomyślnie wysłany.",
+ "verified": "Użytkownik został zaproszony i już zweryfikował swój e-mail.",
+ "notVerifiedYet": "Użytkownik został zaproszony. Otrzyma e-mail z dalszymi instrukcjami.",
+ "submit": "Zaproś kolejnego użytkownika"
+ }
+ },
+ "signedin": {
+ "title": "Witaj {user}!",
+ "description": "Jesteś zalogowany.",
+ "continue": "Kontynuuj",
+ "error": {
+ "title": "Błąd",
+ "description": "Nie można załadować danych. Sprawdź połączenie z internetem lub spróbuj ponownie później."
+ }
+ },
+ "verify": {
+ "userIdMissing": "Nie podano identyfikatora użytkownika!",
+ "successTitle": "Weryfikacja zakończona",
+ "successDescription": "Użytkownik został pomyślnie zweryfikowany.",
+ "setupAuthenticator": "Skonfiguruj uwierzytelnianie",
+ "verify": {
+ "title": "Zweryfikuj użytkownika",
+ "description": "Wprowadź kod z wiadomości weryfikacyjnej.",
+ "noCodeReceived": "Nie otrzymałeś kodu?",
+ "resendCode": "Wyślij kod ponownie",
+ "codeSent": "Kod został właśnie wysłany na twój adres e-mail.",
+ "submit": "Kontynuuj",
+ "required": {
+ "code": "To pole jest wymagane"
+ }
+ }
+ },
+ "authenticator": {
+ "title": "Wybierz metodę uwierzytelniania",
+ "description": "Wybierz metodę, której chcesz użyć do uwierzytelnienia.",
+ "noMethodsAvailable": "Brak dostępnych metod uwierzytelniania",
+ "allSetup": "Już skonfigurowałeś metodę uwierzytelniania!",
+ "linkWithIDP": "lub połącz z dostawcą tożsamości"
+ },
+ "device": {
+ "usercode": {
+ "title": "Kod urządzenia",
+ "description": "Wprowadź kod.",
+ "submit": "Kontynuuj",
+ "required": {
+ "code": "To pole jest wymagane"
+ }
+ },
+ "request": {
+ "title": "{appName} chce się połączyć:",
+ "description": "{appName} będzie miało dostęp do:",
+ "disclaimer": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.",
+ "submit": "Zezwól",
+ "deny": "Odmów"
+ },
+ "scope": {
+ "openid": "Zweryfikuj swoją tożsamość.",
+ "email": "Uzyskaj dostęp do swojego adresu e-mail.",
+ "profile": "Uzyskaj dostęp do pełnych informacji o swoim profilu.",
+ "offline_access": "Zezwól na dostęp offline do swojego konta."
+ }
+ },
+ "error": {
+ "noUserCode": "Nie podano kodu użytkownika!",
+ "noDeviceRequest": "Nie znaleziono żądania urządzenia.",
+ "unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.",
+ "sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.",
+ "failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.",
+ "tryagain": "Spróbuj ponownie"
+ }
+}
diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json
new file mode 100644
index 0000000000..6ba2917e16
--- /dev/null
+++ b/apps/login/locales/ru.json
@@ -0,0 +1,292 @@
+{
+ "common": {
+ "back": "Назад"
+ },
+ "accounts": {
+ "title": "Аккаунты",
+ "description": "Выберите аккаунт, который хотите использовать.",
+ "addAnother": "Добавить другой аккаунт",
+ "noResults": "Аккаунты не найдены",
+ "verified": "проверенный",
+ "expired": "истёк"
+ },
+ "logout": {
+ "title": "Выход",
+ "description": "Выберите аккаунт, который хотите удалить",
+ "noResults": "Аккаунты не найдены",
+ "clear": "Удалить сессию",
+ "verifiedAt": "Последняя активность: {time}",
+ "success": {
+ "title": "Выход выполнен успешно",
+ "description": "Вы успешно вышли из системы."
+ }
+ },
+ "loginname": {
+ "title": "С возвращением!",
+ "description": "Введите свои данные для входа.",
+ "register": "Зарегистрировать нового пользователя",
+ "submit": "Продолжить",
+ "required": {
+ "loginName": "Это поле обязательно для заполнения"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "Пароль",
+ "description": "Введите ваш пароль.",
+ "resetPassword": "Сбросить пароль",
+ "submit": "Продолжить",
+ "required": {
+ "password": "Это поле обязательно для заполнения"
+ }
+ },
+ "set": {
+ "title": "Установить пароль",
+ "description": "Установите пароль для вашего аккаунта",
+ "codeSent": "Код отправлен на ваш адрес электронной почты.",
+ "noCodeReceived": "Не получили код?",
+ "resend": "Отправить код повторно",
+ "submit": "Продолжить",
+ "required": {
+ "code": "Это поле обязательно для заполнения",
+ "newPassword": "Вы должны указать пароль!",
+ "confirmPassword": "Это поле обязательно для заполнения"
+ }
+ },
+ "change": {
+ "title": "Изменить пароль",
+ "description": "Установите пароль для вашего аккаунта",
+ "submit": "Продолжить",
+ "required": {
+ "newPassword": "Вы должны указать новый пароль!",
+ "confirmPassword": "Это поле обязательно для заполнения"
+ }
+ }
+ },
+ "idp": {
+ "title": "Войти через SSO",
+ "description": "Выберите одного из провайдеров для входа",
+ "orSignInWith": "или войти через",
+ "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": "Произошла ошибка при попытке привязать аккаунт."
+ },
+ "completeRegister": {
+ "title": "Завершите регистрацию",
+ "description": "Завершите регистрацию вашего аккаунта."
+ }
+ },
+ "ldap": {
+ "title": "Войти через LDAP",
+ "description": "Введите ваши учетные данные LDAP.",
+ "username": "Имя пользователя",
+ "password": "Пароль",
+ "submit": "Продолжить",
+ "required": {
+ "username": "Это поле обязательно для заполнения",
+ "password": "Это поле обязательно для заполнения"
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "Подтвердите вашу личность",
+ "description": "Выберите один из следующих факторов.",
+ "noResults": "Нет доступных методов двухфакторной аутентификации"
+ },
+ "set": {
+ "title": "Настройка двухфакторной аутентификации",
+ "description": "Выберите один из следующих методов.",
+ "skip": "Пропустить"
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "Подтверждение 2FA",
+ "totpDescription": "Введите код из приложения-аутентификатора.",
+ "smsDescription": "Введите код, полученный по SMS.",
+ "emailDescription": "Введите код, полученный по email.",
+ "noCodeReceived": "Не получили код?",
+ "resendCode": "Отправить код повторно",
+ "submit": "Продолжить",
+ "required": {
+ "code": "Это поле обязательно для заполнения"
+ }
+ },
+ "set": {
+ "title": "Настройка двухфакторной аутентификации",
+ "totpDescription": "Отсканируйте QR-код в приложении-аутентификаторе.",
+ "smsDescription": "Введите номер телефона для получения кода по SMS.",
+ "emailDescription": "Введите email для получения кода.",
+ "totpRegisterDescription": "Отсканируйте QR-код или перейдите по ссылке вручную.",
+ "submit": "Продолжить",
+ "required": {
+ "code": "Это поле обязательно для заполнения"
+ }
+ }
+ },
+ "passkey": {
+ "verify": {
+ "title": "Аутентификация с помощью пасскей",
+ "description": "Устройство запросит отпечаток пальца, лицо или экранный замок",
+ "usePassword": "Использовать пароль",
+ "submit": "Продолжить"
+ },
+ "set": {
+ "title": "Настройка пасскей",
+ "description": "Устройство запросит отпечаток пальца, лицо или экранный замок",
+ "info": {
+ "description": "Пасскей — метод аутентификации через устройство (отпечаток пальца, Apple FaceID и аналоги).",
+ "link": "Аутентификация без пароля"
+ },
+ "skip": "Пропустить",
+ "submit": "Продолжить"
+ }
+ },
+ "u2f": {
+ "verify": {
+ "title": "Подтверждение 2FA",
+ "description": "Подтвердите аккаунт с помощью устройства."
+ },
+ "set": {
+ "title": "Настройка двухфакторной аутентификации",
+ "description": "Настройте устройство как второй фактор.",
+ "submit": "Продолжить"
+ }
+ },
+ "register": {
+ "methods": {
+ "passkey": "Пасскей",
+ "password": "Пароль"
+ },
+ "disabled": {
+ "title": "Регистрация отключена",
+ "description": "Регистрация недоступна. Обратитесь к администратору."
+ },
+ "missingdata": {
+ "title": "Недостаточно данных",
+ "description": "Укажите email, имя и фамилию для регистрации."
+ },
+ "title": "Регистрация",
+ "description": "Создайте свой аккаунт ZITADEL.",
+ "noMethodAvailableWarning": "Нет доступных методов аутентификации. Обратитесь к администратору.",
+ "selectMethod": "Выберите метод аутентификации",
+ "agreeTo": "Для регистрации необходимо принять условия:",
+ "termsOfService": "Условия использования",
+ "privacyPolicy": "Политика конфиденциальности",
+ "submit": "Продолжить",
+ "orUseIDP": "или используйте Identity Provider",
+ "password": {
+ "title": "Установить пароль",
+ "description": "Установите пароль для вашего аккаунта",
+ "submit": "Продолжить",
+ "required": {
+ "password": "Вы должны указать пароль!",
+ "confirmPassword": "Это поле обязательно для заполнения"
+ }
+ },
+ "required": {
+ "firstname": "Это поле обязательно для заполнения",
+ "lastname": "Это поле обязательно для заполнения",
+ "email": "Это поле обязательно для заполнения"
+ }
+ },
+ "invite": {
+ "title": "Пригласить пользователя",
+ "description": "Укажите email и имя пользователя для приглашения.",
+ "info": "Пользователь получит email с инструкциями.",
+ "notAllowed": "Ваши настройки не позволяют приглашать пользователей.",
+ "submit": "Продолжить",
+ "success": {
+ "title": "Пользователь приглашён",
+ "description": "Письмо успешно отправлено.",
+ "verified": "Пользователь приглашён и уже подтвердил email.",
+ "notVerifiedYet": "Пользователь приглашён. Он получит email с инструкциями.",
+ "submit": "Пригласить другого пользователя"
+ }
+ },
+ "signedin": {
+ "title": "Добро пожаловать, {user}!",
+ "description": "Вы вошли в систему.",
+ "continue": "Продолжить",
+ "error": {
+ "title": "Ошибка",
+ "description": "Не удалось войти в систему. Проверьте свои данные и попробуйте снова."
+ }
+ },
+ "verify": {
+ "userIdMissing": "Не указан userId!",
+ "successTitle": "Пользователь подтверждён",
+ "successDescription": "Пользователь успешно подтверждён.",
+ "setupAuthenticator": "Настроить аутентификатор",
+ "verify": {
+ "title": "Подтверждение пользователя",
+ "description": "Введите код из письма подтверждения.",
+ "noCodeReceived": "Не получили код?",
+ "resendCode": "Отправить код повторно",
+ "codeSent": "Код отправлен на ваш email.",
+ "submit": "Продолжить",
+ "required": {
+ "code": "Это поле обязательно для заполнения"
+ }
+ }
+ },
+ "authenticator": {
+ "title": "Выбор метода аутентификации",
+ "description": "Выберите предпочитаемый метод аутентификации",
+ "noMethodsAvailable": "Нет доступных методов аутентификации",
+ "allSetup": "Аутентификатор уже настроен!",
+ "linkWithIDP": "или привязать через Identity Provider"
+ },
+ "device": {
+ "usercode": {
+ "title": "Код устройства",
+ "description": "Введите код.",
+ "submit": "Продолжить",
+ "required": {
+ "code": "Это поле обязательно для заполнения"
+ }
+ },
+ "request": {
+ "title": "{appName} хочет подключиться:",
+ "description": "{appName} получит доступ к:",
+ "disclaimer": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.",
+ "submit": "Разрешить",
+ "deny": "Запретить"
+ },
+ "scope": {
+ "openid": "Проверка вашей личности.",
+ "email": "Доступ к вашему адресу электронной почты.",
+ "profile": "Доступ к полной информации вашего профиля.",
+ "offline_access": "Разрешить офлайн-доступ к вашему аккаунту."
+ }
+ },
+ "error": {
+ "noUserCode": "Не указан код пользователя!",
+ "noDeviceRequest": "Не найдена ни одна заявка на устройство.",
+ "unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.",
+ "sessionExpired": "Ваша сессия истекла. Войдите снова.",
+ "failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.",
+ "tryagain": "Попробовать снова"
+ }
+}
diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json
new file mode 100644
index 0000000000..fe5f2d1867
--- /dev/null
+++ b/apps/login/locales/zh.json
@@ -0,0 +1,292 @@
+{
+ "common": {
+ "back": "返回"
+ },
+ "accounts": {
+ "title": "账户",
+ "description": "选择您要使用的账户。",
+ "addAnother": "添加另一个账户",
+ "noResults": "未找到账户",
+ "verified": "已验证",
+ "expired": "已过期"
+ },
+ "logout": {
+ "title": "注销",
+ "description": "选择您想注销的账户",
+ "noResults": "未找到账户",
+ "clear": "注销会话",
+ "verifiedAt": "最后活动时间:{time}",
+ "success": {
+ "title": "注销成功",
+ "description": "您已成功注销。"
+ }
+ },
+ "loginname": {
+ "title": "欢迎回来!",
+ "description": "请输入您的登录信息。",
+ "register": "注册新用户",
+ "submit": "继续",
+ "required": {
+ "loginName": "此字段为必填项"
+ }
+ },
+ "password": {
+ "verify": {
+ "title": "密码",
+ "description": "请输入您的密码。",
+ "resetPassword": "重置密码",
+ "submit": "继续",
+ "required": {
+ "password": "此字段为必填项"
+ }
+ },
+ "set": {
+ "title": "设置密码",
+ "description": "为您的账户设置密码",
+ "codeSent": "验证码已发送到您的邮箱。",
+ "noCodeReceived": "没有收到验证码?",
+ "resend": "重发验证码",
+ "submit": "继续",
+ "required": {
+ "code": "此字段为必填项",
+ "newPassword": "必须提供密码!",
+ "confirmPassword": "此字段为必填项"
+ }
+ },
+ "change": {
+ "title": "更改密码",
+ "description": "为您的账户设置密码",
+ "submit": "继续",
+ "required": {
+ "newPassword": "必须提供新密码!",
+ "confirmPassword": "此字段为必填项"
+ }
+ }
+ },
+ "idp": {
+ "title": "使用 SSO 登录",
+ "description": "选择以下提供商中的一个进行登录",
+ "orSignInWith": "或使用以下方式登录",
+ "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": "链接账户时发生错误。"
+ },
+ "completeRegister": {
+ "title": "完成注册",
+ "description": "完成您的账户注册。"
+ }
+ },
+ "ldap": {
+ "title": "使用 LDAP 登录",
+ "description": "请输入您的 LDAP 凭据。",
+ "username": "用户名",
+ "password": "密码",
+ "submit": "继续",
+ "required": {
+ "username": "此字段为必填项",
+ "password": "此字段为必填项"
+ }
+ },
+ "mfa": {
+ "verify": {
+ "title": "验证您的身份",
+ "description": "选择以下的一个因素。",
+ "noResults": "没有可设置的第二因素。"
+ },
+ "set": {
+ "title": "设置双因素认证",
+ "description": "选择以下的一个第二因素。",
+ "skip": "跳过"
+ }
+ },
+ "otp": {
+ "verify": {
+ "title": "验证双因素",
+ "totpDescription": "请输入认证应用程序中的验证码。",
+ "smsDescription": "输入通过短信收到的验证码。",
+ "emailDescription": "输入通过电子邮件收到的验证码。",
+ "noCodeReceived": "没有收到验证码?",
+ "resendCode": "重发验证码",
+ "submit": "继续",
+ "required": {
+ "code": "此字段为必填项"
+ }
+ },
+ "set": {
+ "title": "设置双因素认证",
+ "totpDescription": "使用认证应用程序扫描二维码。",
+ "smsDescription": "输入您的电话号码以接收短信验证码。",
+ "emailDescription": "输入您的电子邮箱地址以接收电子邮件验证码。",
+ "totpRegisterDescription": "扫描二维码或手动导航到URL。",
+ "submit": "继续",
+ "required": {
+ "code": "此字段为必填项"
+ }
+ }
+ },
+ "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 账户。",
+ "noMethodAvailableWarning": "没有可用的认证方法。请联系您的系统管理员。",
+ "selectMethod": "选择您想使用的认证方法",
+ "agreeTo": "注册即表示您同意条款和条件",
+ "termsOfService": "服务条款",
+ "privacyPolicy": "隐私政策",
+ "submit": "继续",
+ "orUseIDP": "或使用身份提供者",
+ "password": {
+ "title": "设置密码",
+ "description": "为您的账户设置密码",
+ "submit": "继续",
+ "required": {
+ "password": "必须提供密码!",
+ "confirmPassword": "此字段为必填项"
+ }
+ },
+ "required": {
+ "firstname": "此字段为必填项",
+ "lastname": "此字段为必填项",
+ "email": "此字段为必填项"
+ }
+ },
+ "invite": {
+ "title": "邀请用户",
+ "description": "提供您想邀请的用户的电子邮箱地址和姓名。",
+ "info": "用户将收到一封包含进一步说明的电子邮件。",
+ "notAllowed": "您的设置不允许邀请用户。",
+ "submit": "继续",
+ "success": {
+ "title": "用户已邀请",
+ "description": "邮件已成功发送。",
+ "verified": "用户已被邀请并已验证其电子邮件。",
+ "notVerifiedYet": "用户已被邀请。他们将收到一封包含进一步说明的电子邮件。",
+ "submit": "邀请另一位用户"
+ }
+ },
+ "signedin": {
+ "title": "欢迎 {user}!",
+ "description": "您已登录。",
+ "continue": "继续",
+ "error": {
+ "title": "错误",
+ "description": "登录时发生错误。"
+ }
+ },
+ "verify": {
+ "userIdMissing": "未提供用户 ID!",
+ "successTitle": "用户已验证",
+ "successDescription": "用户已成功验证。",
+ "setupAuthenticator": "设置认证器",
+ "verify": {
+ "title": "验证用户",
+ "description": "输入验证邮件中的验证码。",
+ "noCodeReceived": "没有收到验证码?",
+ "resendCode": "重发验证码",
+ "codeSent": "刚刚发送了一封包含验证码的电子邮件。",
+ "submit": "继续",
+ "required": {
+ "code": "此字段为必填项"
+ }
+ }
+ },
+ "authenticator": {
+ "title": "选择认证方式",
+ "description": "选择您想使用的认证方法",
+ "noMethodsAvailable": "没有可用的认证方法",
+ "allSetup": "您已经设置好了一个认证器!",
+ "linkWithIDP": "或将其与身份提供者关联"
+ },
+ "device": {
+ "usercode": {
+ "title": "设备代码",
+ "description": "输入代码。",
+ "submit": "继续",
+ "required": {
+ "code": "此字段为必填项"
+ }
+ },
+ "request": {
+ "title": "{appName} 想要连接:",
+ "description": "{appName} 将访问:",
+ "disclaimer": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。",
+ "submit": "允许",
+ "deny": "拒绝"
+ },
+ "scope": {
+ "openid": "验证您的身份。",
+ "email": "访问您的电子邮件地址。",
+ "profile": "访问您的完整个人资料信息。",
+ "offline_access": "允许离线访问您的账户。"
+ }
+ },
+ "error": {
+ "noUserCode": "未提供用户代码!",
+ "noDeviceRequest": "没有找到设备请求。",
+ "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。",
+ "sessionExpired": "当前会话已过期,请重新登录。",
+ "failedLoading": "加载数据失败,请再试一次。",
+ "tryagain": "重试"
+ }
+}
diff --git a/apps/login/next-env-vars.d.ts b/apps/login/next-env-vars.d.ts
new file mode 100644
index 0000000000..b7a525858c
--- /dev/null
+++ b/apps/login/next-env-vars.d.ts
@@ -0,0 +1,33 @@
+declare namespace NodeJS {
+ interface ProcessEnv {
+ // Allow any environment variable that matches the pattern
+ [key: `${string}_AUDIENCE`]: string; // The system api url
+ [key: `${string}_SYSTEM_USER_ID`]: string; // The service user id
+ [key: `${string}_SYSTEM_USER_PRIVATE_KEY`]: string; // The service user private key
+
+ AUDIENCE: string; // The fallback system api url
+ SYSTEM_USER_ID: string; // The fallback service user id
+ SYSTEM_USER_PRIVATE_KEY: string; // The fallback service user private key
+
+ /**
+ * The Zitadel API url
+ */
+ ZITADEL_API_URL: string;
+
+ /**
+ * The service user token
+ */
+ ZITADEL_SERVICE_USER_TOKEN: string;
+
+ /**
+ * Optional: wheter a user must have verified email
+ */
+ EMAIL_VERIFICATION: string;
+
+ /**
+ * Optional: custom request headers to be added to every request
+ * Split by comma, key value pairs separated by colon
+ */
+ CUSTOM_REQUEST_HEADERS?: string;
+ }
+}
diff --git a/apps/login/next-env.d.ts b/apps/login/next-env.d.ts
new file mode 100755
index 0000000000..1b3be0840f
--- /dev/null
+++ b/apps/login/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// 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
new file mode 100755
index 0000000000..b84f11a230
--- /dev/null
+++ b/apps/login/next.config.mjs
@@ -0,0 +1,83 @@
+import createNextIntlPlugin from "next-intl/plugin";
+import { DEFAULT_CSP } from "./constants/csp.js";
+
+const withNextIntl = createNextIntlPlugin();
+
+/** @type {import('next').NextConfig} */
+
+const secureHeaders = [
+ {
+ key: "Strict-Transport-Security",
+ value: "max-age=63072000; includeSubDomains; preload",
+ },
+ {
+ key: "Referrer-Policy",
+ value: "origin-when-cross-origin",
+ },
+ {
+ key: "X-Frame-Options",
+ value: "SAMEORIGIN",
+ },
+ {
+ key: "X-Content-Type-Options",
+ value: "nosniff",
+ },
+ {
+ key: "X-XSS-Protection",
+ value: "1; mode=block",
+ },
+ {
+ key: "Content-Security-Policy",
+ value: `${DEFAULT_CSP} frame-ancestors 'none'`,
+ },
+ { key: "X-Frame-Options", value: "deny" },
+];
+
+const imageRemotePatterns = [
+ {
+ protocol: "http",
+ hostname: "localhost",
+ port: "8080",
+ pathname: "/**",
+ },
+ {
+ protocol: "https",
+ hostname: "*.zitadel.*",
+ port: "",
+ pathname: "/**",
+ },
+];
+
+if (process.env.ZITADEL_API_URL) {
+ imageRemotePatterns.push({
+ protocol: "https",
+ hostname: process.env.ZITADEL_API_URL?.replace("https://", "") || "",
+ port: "",
+ pathname: "/**",
+ });
+}
+
+const nextConfig = {
+ basePath: process.env.NEXT_PUBLIC_BASE_PATH,
+ output: process.env.NEXT_OUTPUT_MODE || undefined,
+ reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
+ experimental: {
+ dynamicIO: true,
+ },
+ images: {
+ remotePatterns: imageRemotePatterns,
+ },
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ async headers() {
+ return [
+ {
+ source: "/:path*",
+ headers: secureHeaders,
+ },
+ ];
+ },
+};
+
+export default withNextIntl(nextConfig);
diff --git a/apps/login/package.json b/apps/login/package.json
new file mode 100644
index 0000000000..a4e02922ee
--- /dev/null
+++ b/apps/login/package.json
@@ -0,0 +1,100 @@
+{
+ "packageManager": "pnpm@9.1.2+sha256.19c17528f9ca20bd442e4ca42f00f1b9808a9cb419383cd04ba32ef19322aba7",
+ "name": "@zitadel/login",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "build:login:standalone": "NEXT_PUBLIC_BASE_PATH=/ui/v2/login NEXT_OUTPUT_MODE=standalone next build",
+ "start": "next start",
+ "lint": "pnpm run '/^lint:check:.*$/'",
+ "lint:check:next": "next lint",
+ "lint:check:prettier": "prettier --check .",
+ "lint:fix": "prettier --write .",
+ "test:unit": "vitest --run",
+ "lint-staged": "lint-staged",
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
+ "test:integration:login": "cypress run",
+ "test:integration:login:debug": "cypress open",
+ "test:acceptance": "dotenv -e ../login/.env.test.local playwright",
+ "test:acceptance:setup": "cd ../.. && make login_test_acceptance_setup_env && NODE_ENV=test turbo run test:acceptance:setup:dev",
+ "test:acceptance:setup:dev": "cd ../.. && make login_test_acceptance_setup_dev"
+ },
+ "git": {
+ "pre-commit": "lint-staged"
+ },
+ "lint-staged": {
+ "*": "prettier --write --ignore-unknown"
+ },
+ "dependencies": {
+ "@headlessui/react": "^2.1.9",
+ "@heroicons/react": "2.1.3",
+ "@radix-ui/react-tooltip": "^1.2.7",
+ "@tailwindcss/forms": "0.5.7",
+ "@vercel/analytics": "^1.2.2",
+ "@zitadel/client": "latest",
+ "@zitadel/proto": "latest",
+ "clsx": "1.2.1",
+ "copy-to-clipboard": "^3.3.3",
+ "deepmerge": "^4.3.1",
+ "lucide-react": "0.469.0",
+ "moment": "^2.29.4",
+ "next": "15.4.0-canary.86",
+ "next-intl": "^3.25.1",
+ "next-themes": "^0.2.1",
+ "nice-grpc": "2.0.1",
+ "qrcode.react": "^3.1.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-hook-form": "7.39.5",
+ "tinycolor2": "1.4.2",
+ "uuid": "^11.1.0"
+ },
+ "devDependencies": {
+ "@babel/eslint-parser": "^7.23.0",
+ "@bufbuild/buf": "^1.53.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@types/ms": "2.1.0",
+ "@types/node": "^22.14.1",
+ "@types/react": "19.1.2",
+ "@types/react-dom": "19.1.2",
+ "@types/tinycolor2": "1.4.3",
+ "@types/uuid": "^10.0.0",
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
+ "@typescript-eslint/parser": "^7.0.0",
+ "@vercel/git-hooks": "1.0.0",
+ "@vitejs/plugin-react": "^4.4.1",
+ "autoprefixer": "10.4.21",
+ "eslint": "^8.57.0",
+ "eslint-config-next": "15.4.0-canary.86",
+ "eslint-config-prettier": "^9.1.0",
+ "grpc-tools": "1.13.0",
+ "jsdom": "^26.1.0",
+ "lint-staged": "15.5.1",
+ "make-dir-cli": "4.0.0",
+ "postcss": "8.5.3",
+ "prettier": "^3.2.5",
+ "prettier-plugin-organize-imports": "^3.2.0",
+ "prettier-plugin-tailwindcss": "0.6.11",
+ "sass": "^1.87.0",
+ "tailwindcss": "3.4.14",
+ "ts-proto": "^2.7.0",
+ "typescript": "^5.8.3",
+ "vite-tsconfig-paths": "^5.1.4",
+ "vitest": "^2.0.0",
+ "concurrently": "^9.1.2",
+ "cypress": "^14.5.2",
+ "dotenv-cli": "^8.0.0",
+ "env-cmd": "^10.0.0",
+ "nodemon": "^3.1.9",
+ "start-server-and-test": "^2.0.11",
+ "@faker-js/faker": "^9.7.0",
+ "@otplib/core": "^12.0.0",
+ "@otplib/plugin-crypto": "^12.0.0",
+ "@otplib/plugin-thirty-two": "^12.0.0",
+ "@playwright/test": "^1.52.0",
+ "gaxios": "^7.1.0"
+ }
+}
diff --git a/apps/login/postcss.config.cjs b/apps/login/postcss.config.cjs
new file mode 100644
index 0000000000..12a703d900
--- /dev/null
+++ b/apps/login/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/login/prettier.config.mjs b/apps/login/prettier.config.mjs
new file mode 100644
index 0000000000..5e0f9b2584
--- /dev/null
+++ b/apps/login/prettier.config.mjs
@@ -0,0 +1,11 @@
+export default {
+ printWidth: 80,
+ tabWidth: 2,
+ useTabs: false,
+ semi: true,
+ singleQuote: false,
+ trailingComma: "all",
+ bracketSpacing: true,
+ arrowParens: "always",
+ plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"],
+};
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/favicon.ico b/apps/login/public/favicon.ico
new file mode 100644
index 0000000000..a901eddc34
Binary files /dev/null and b/apps/login/public/favicon.ico differ
diff --git a/apps/login/public/favicon/android-chrome-192x192.png b/apps/login/public/favicon/android-chrome-192x192.png
new file mode 100644
index 0000000000..f22bd442e6
Binary files /dev/null and b/apps/login/public/favicon/android-chrome-192x192.png differ
diff --git a/apps/login/public/favicon/android-chrome-512x512.png b/apps/login/public/favicon/android-chrome-512x512.png
new file mode 100644
index 0000000000..6987ed11b4
Binary files /dev/null and b/apps/login/public/favicon/android-chrome-512x512.png differ
diff --git a/apps/login/public/favicon/apple-touch-icon.png b/apps/login/public/favicon/apple-touch-icon.png
new file mode 100644
index 0000000000..4816102015
Binary files /dev/null and b/apps/login/public/favicon/apple-touch-icon.png differ
diff --git a/apps/login/public/favicon/browserconfig.xml b/apps/login/public/favicon/browserconfig.xml
new file mode 100644
index 0000000000..75efb24254
--- /dev/null
+++ b/apps/login/public/favicon/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #000000
+
+
+
diff --git a/apps/login/public/favicon/favicon-16x16.png b/apps/login/public/favicon/favicon-16x16.png
new file mode 100644
index 0000000000..4f45702ca9
Binary files /dev/null and b/apps/login/public/favicon/favicon-16x16.png differ
diff --git a/apps/login/public/favicon/favicon-32x32.png b/apps/login/public/favicon/favicon-32x32.png
new file mode 100644
index 0000000000..a598da05eb
Binary files /dev/null and b/apps/login/public/favicon/favicon-32x32.png differ
diff --git a/apps/login/public/favicon/favicon.ico b/apps/login/public/favicon/favicon.ico
new file mode 100644
index 0000000000..af98450595
Binary files /dev/null and b/apps/login/public/favicon/favicon.ico differ
diff --git a/apps/login/public/favicon/mstile-150x150.png b/apps/login/public/favicon/mstile-150x150.png
new file mode 100644
index 0000000000..ab518480e6
Binary files /dev/null and b/apps/login/public/favicon/mstile-150x150.png differ
diff --git a/apps/login/public/favicon/site.webmanifest b/apps/login/public/favicon/site.webmanifest
new file mode 100644
index 0000000000..567c2c6549
--- /dev/null
+++ b/apps/login/public/favicon/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "ZITADEL Login",
+ "short_name": "ZITADEL Login",
+ "icons": [
+ {
+ "src": "/favicon/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/favicon/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#000000",
+ "background_color": "#000000",
+ "display": "standalone"
+}
diff --git a/apps/login/public/grid-dark.svg b/apps/login/public/grid-dark.svg
new file mode 100644
index 0000000000..d467ad6de0
--- /dev/null
+++ b/apps/login/public/grid-dark.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/apps/login/public/grid-light.svg b/apps/login/public/grid-light.svg
new file mode 100644
index 0000000000..114c1186fe
--- /dev/null
+++ b/apps/login/public/grid-light.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg
new file mode 100644
index 0000000000..4a4e8be71b
--- /dev/null
+++ b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg
@@ -0,0 +1,74 @@
+
+
+
diff --git a/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg b/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg
new file mode 100644
index 0000000000..33ea6b583b
--- /dev/null
+++ b/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg
@@ -0,0 +1,76 @@
+
+
+
diff --git a/apps/login/public/zitadel-logo-dark.svg b/apps/login/public/zitadel-logo-dark.svg
new file mode 100644
index 0000000000..6dcfe06e6d
--- /dev/null
+++ b/apps/login/public/zitadel-logo-dark.svg
@@ -0,0 +1,101 @@
+
+
+
diff --git a/apps/login/public/zitadel-logo-light.svg b/apps/login/public/zitadel-logo-light.svg
new file mode 100644
index 0000000000..d48a5eeb94
--- /dev/null
+++ b/apps/login/public/zitadel-logo-light.svg
@@ -0,0 +1,99 @@
+
+
+
diff --git a/apps/login/screenshots/accounts.png b/apps/login/screenshots/accounts.png
new file mode 100644
index 0000000000..a8591141c6
Binary files /dev/null and b/apps/login/screenshots/accounts.png differ
diff --git a/apps/login/screenshots/accounts_jumpto.png b/apps/login/screenshots/accounts_jumpto.png
new file mode 100644
index 0000000000..0fd126bf4c
Binary files /dev/null and b/apps/login/screenshots/accounts_jumpto.png differ
diff --git a/apps/login/screenshots/collage.png b/apps/login/screenshots/collage.png
new file mode 100644
index 0000000000..9d5a9c35c8
Binary files /dev/null and b/apps/login/screenshots/collage.png differ
diff --git a/apps/login/screenshots/idp.png b/apps/login/screenshots/idp.png
new file mode 100644
index 0000000000..9bf58c69b0
Binary files /dev/null and b/apps/login/screenshots/idp.png differ
diff --git a/apps/login/screenshots/loginname.png b/apps/login/screenshots/loginname.png
new file mode 100644
index 0000000000..342e60799e
Binary files /dev/null and b/apps/login/screenshots/loginname.png differ
diff --git a/apps/login/screenshots/mfa.png b/apps/login/screenshots/mfa.png
new file mode 100644
index 0000000000..1fd73f205c
Binary files /dev/null and b/apps/login/screenshots/mfa.png differ
diff --git a/apps/login/screenshots/mfaset.png b/apps/login/screenshots/mfaset.png
new file mode 100644
index 0000000000..c00ee4edf5
Binary files /dev/null and b/apps/login/screenshots/mfaset.png differ
diff --git a/apps/login/screenshots/otp.png b/apps/login/screenshots/otp.png
new file mode 100644
index 0000000000..3818a5ad5f
Binary files /dev/null and b/apps/login/screenshots/otp.png differ
diff --git a/apps/login/screenshots/otpset.png b/apps/login/screenshots/otpset.png
new file mode 100644
index 0000000000..f75c2154c7
Binary files /dev/null and b/apps/login/screenshots/otpset.png differ
diff --git a/apps/login/screenshots/passkey.png b/apps/login/screenshots/passkey.png
new file mode 100644
index 0000000000..7a5686c736
Binary files /dev/null and b/apps/login/screenshots/passkey.png differ
diff --git a/apps/login/screenshots/password.png b/apps/login/screenshots/password.png
new file mode 100644
index 0000000000..05cf8747bb
Binary files /dev/null and b/apps/login/screenshots/password.png differ
diff --git a/apps/login/screenshots/password_change.png b/apps/login/screenshots/password_change.png
new file mode 100644
index 0000000000..183de6df34
Binary files /dev/null and b/apps/login/screenshots/password_change.png differ
diff --git a/apps/login/screenshots/password_set.png b/apps/login/screenshots/password_set.png
new file mode 100644
index 0000000000..15b5ff49ad
Binary files /dev/null and b/apps/login/screenshots/password_set.png differ
diff --git a/apps/login/screenshots/register.png b/apps/login/screenshots/register.png
new file mode 100644
index 0000000000..ba9f6951d8
Binary files /dev/null and b/apps/login/screenshots/register.png differ
diff --git a/apps/login/screenshots/register_password.png b/apps/login/screenshots/register_password.png
new file mode 100644
index 0000000000..31515bda9a
Binary files /dev/null and b/apps/login/screenshots/register_password.png differ
diff --git a/apps/login/screenshots/signedin.png b/apps/login/screenshots/signedin.png
new file mode 100644
index 0000000000..f96ea1721f
Binary files /dev/null and b/apps/login/screenshots/signedin.png differ
diff --git a/apps/login/screenshots/u2f.png b/apps/login/screenshots/u2f.png
new file mode 100644
index 0000000000..6b8eca087d
Binary files /dev/null and b/apps/login/screenshots/u2f.png differ
diff --git a/apps/login/screenshots/u2fset.png b/apps/login/screenshots/u2fset.png
new file mode 100644
index 0000000000..37115548a5
Binary files /dev/null and b/apps/login/screenshots/u2fset.png differ
diff --git a/apps/login/screenshots/verify.png b/apps/login/screenshots/verify.png
new file mode 100644
index 0000000000..c13e6a3a88
Binary files /dev/null and b/apps/login/screenshots/verify.png differ
diff --git a/apps/login/scripts/entrypoint.sh b/apps/login/scripts/entrypoint.sh
new file mode 100755
index 0000000000..123612d1cb
--- /dev/null
+++ b/apps/login/scripts/entrypoint.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -o allexport
+. /.env-file/.env
+set +o allexport
+
+if [ -n "${ZITADEL_SERVICE_USER_TOKEN_FILE}" ] && [ -f "${ZITADEL_SERVICE_USER_TOKEN_FILE}" ]; then
+ echo "ZITADEL_SERVICE_USER_TOKEN_FILE=${ZITADEL_SERVICE_USER_TOKEN_FILE} is set and file exists, setting ZITADEL_SERVICE_USER_TOKEN to the files content"
+ export ZITADEL_SERVICE_USER_TOKEN=$(cat "${ZITADEL_SERVICE_USER_TOKEN_FILE}")
+fi
+
+
+
+exec node /runtime/apps/login/apps/login/server.js
diff --git a/apps/login/scripts/healthcheck.js b/apps/login/scripts/healthcheck.js
new file mode 100644
index 0000000000..652524154a
--- /dev/null
+++ b/apps/login/scripts/healthcheck.js
@@ -0,0 +1,14 @@
+const url = process.argv[2];
+
+if (!url) {
+ console.error("❌ No URL provided as command line argument.");
+ process.exit(1);
+}
+
+try {
+ const res = await fetch(url);
+ if (!res.ok) process.exit(1);
+ process.exit(0);
+} catch (e) {
+ process.exit(1);
+}
diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx
new file mode 100644
index 0000000000..e4e6b387dc
--- /dev/null
+++ b/apps/login/src/app/(login)/accounts/page.tsx
@@ -0,0 +1,96 @@
+import { DynamicTheme } from "@/components/dynamic-theme";
+import { SessionsList } from "@/components/sessions-list";
+import { Translated } from "@/components/translated";
+import { getAllSessionCookieIds } from "@/lib/cookies";
+import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import {
+ getBrandingSettings,
+ getDefaultOrg,
+ listSessions,
+} from "@/lib/zitadel";
+import { UserPlusIcon } from "@heroicons/react/24/outline";
+import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
+// import { getLocale } from "next-intl/server";
+import { headers } from "next/headers";
+import Link from "next/link";
+
+async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
+ const cookieIds = await getAllSessionCookieIds();
+
+ if (cookieIds && cookieIds.length) {
+ const response = await listSessions({
+ serviceUrl,
+ ids: cookieIds.filter((id) => !!id) as string[],
+ });
+ return response?.sessions ?? [];
+ } else {
+ console.info("No session cookie found.");
+ return [];
+ }
+}
+
+export default async function Page(props: {
+ searchParams: Promise>;
+}) {
+ const searchParams = await props.searchParams;
+
+ const requestId = searchParams?.requestId;
+ const organization = searchParams?.organization;
+
+ const _headers = await headers();
+ const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+
+ let defaultOrganization;
+ if (!organization) {
+ const org: Organization | null = await getDefaultOrg({
+ serviceUrl,
+ });
+ if (org) {
+ defaultOrganization = org.id;
+ }
+ }
+
+ let sessions = await loadSessions({ serviceUrl });
+
+ const branding = await getBrandingSettings({
+ serviceUrl,
+ organization: organization ?? defaultOrganization,
+ });
+
+ const params = new URLSearchParams();
+
+ if (requestId) {
+ params.append("requestId", requestId);
+ }
+
+ if (organization) {
+ params.append("organization", organization);
+ }
+
+ return (
+
+
+
+ {/* show error only if usernames should be shown to be unknown */}
+ {(!sessionFactors || !loginName) &&
+ !loginSettings?.ignoreUnknownUsernames && (
+
+
+ {/* show error only if usernames should be shown to be unknown */}
+ {(!sessionFactors || !loginName) &&
+ !loginSettings?.ignoreUnknownUsernames && (
+
+
+
+
+ {requestId && requestId.startsWith("device_") && (
+
+ You can now close this window and return to the device where you
+ started the authorization process to continue.
+
+ )}
+
+ {loginSettings?.defaultRedirectUri && (
+