mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 07:57:32 +00:00
fixup! fixup! fixup! Merge branch 'main' into fix_adding_org_same_id_twice
This commit is contained in:
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@@ -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:
|
||||
|
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@@ -18,6 +18,8 @@ permissions:
|
||||
packages: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
core:
|
||||
@@ -47,6 +49,7 @@ 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"
|
||||
|
||||
core-unit-test:
|
||||
needs: core
|
||||
@@ -76,6 +79,16 @@ jobs:
|
||||
core_cache_key: ${{ needs.core.outputs.cache_key }}
|
||||
core_cache_path: ${{ needs.core.outputs.cache_path }}
|
||||
|
||||
login-quality:
|
||||
needs: [compile]
|
||||
uses: ./.github/workflows/login-quality.yml
|
||||
permissions:
|
||||
actions: write
|
||||
id-token: write
|
||||
with:
|
||||
ignore-run-cache: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
node_version: "20"
|
||||
|
||||
container:
|
||||
needs: [compile]
|
||||
uses: ./.github/workflows/container.yml
|
||||
@@ -86,6 +99,16 @@ jobs:
|
||||
with:
|
||||
build_image_name: "ghcr.io/zitadel/zitadel-build"
|
||||
|
||||
login-container:
|
||||
uses: ./.github/workflows/login-container.yml
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
permissions:
|
||||
packages: write
|
||||
id-token: write
|
||||
with:
|
||||
login_build_image_name: "ghcr.io/zitadel/login-build"
|
||||
node_version: "20"
|
||||
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
needs: [compile]
|
||||
@@ -98,7 +121,7 @@ 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, login-quality, e2e]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
secrets:
|
||||
GCR_JSON_KEY_BASE64: ${{ secrets.GCR_JSON_KEY_BASE64 }}
|
||||
@@ -109,3 +132,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/login"
|
||||
google_image_name_login: europe-docker.pkg.dev/zitadel-common/zitadel-repo/login
|
||||
|
36
.github/workflows/compile.yml
vendored
36
.github/workflows/compile.yml
vendored
@@ -18,7 +18,9 @@ on:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
node_version:
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
executable:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -73,10 +75,38 @@ jobs:
|
||||
with:
|
||||
name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
|
||||
|
||||
login:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
uses: depot/setup-action@v1
|
||||
with:
|
||||
oidc: true
|
||||
-
|
||||
run: make login_standalone_out
|
||||
env:
|
||||
# latest if branch is main, otherwise image version which is the pull request number
|
||||
LOGIN_BAKE_CLI: depot bake
|
||||
DEPOT_PROJECT_ID: w47wkxzdtw
|
||||
NODE_VERSION: ${{ inputs.node_version }}
|
||||
-
|
||||
name: move files
|
||||
run: |
|
||||
cp login/LICENSE login/apps/login/standalone/
|
||||
cp login/README.md login/apps/login/standalone/
|
||||
tar -czvf login.tar.gz -C login/apps/login/standalone .
|
||||
-
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: login
|
||||
path: login.tar.gz
|
||||
|
||||
checksums:
|
||||
runs-on: ubuntu-latest
|
||||
needs: executable
|
||||
needs: [executable, login]
|
||||
steps:
|
||||
-
|
||||
uses: actions/download-artifact@v4
|
||||
|
63
.github/workflows/login-container.yml
vendored
Normal file
63
.github/workflows/login-container.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
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 }}'
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
env:
|
||||
default_labels: |
|
||||
org.opencontainers.image.documentation=https://zitadel.com/docs
|
||||
org.opencontainers.image.vendor=CAOS AG
|
||||
|
||||
jobs:
|
||||
login-container:
|
||||
name: Build Login Container
|
||||
runs-on: depot-ubuntu-22.04-8
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: depot/setup-action@v1
|
||||
with:
|
||||
oidc: true
|
||||
- name: Login meta
|
||||
id: login-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ inputs.login_build_image_name }}
|
||||
labels: ${{ env.default_labels}}
|
||||
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:
|
||||
NODE_VERSION: ${{ inputs.node_version }}
|
||||
with:
|
||||
workdir: login
|
||||
push: true
|
||||
targets: login-standalone
|
||||
set: login-standalone.platforms=[linux/amd64,linux/arm64]
|
||||
project: w47wkxzdtw
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
cwd://${{ steps.login-meta.outputs.bake-file }}
|
59
.github/workflows/login-quality.yml
vendored
Normal file
59
.github/workflows/login-quality.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Login Quality
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ignore-run-cache:
|
||||
description: 'Ignore run caches'
|
||||
type: boolean
|
||||
required: true
|
||||
node_version:
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
quality:
|
||||
name: Ensure Quality
|
||||
runs-on: depot-ubuntu-22.04-8
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
actions: write
|
||||
env:
|
||||
CACHE_DIR: /tmp/login-run-caches
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: depot/setup-action@v1
|
||||
with:
|
||||
oidc: true
|
||||
- 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-
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: .artifacts
|
||||
name: zitadel-linux-amd64
|
||||
- name: Unpack executable
|
||||
run: |
|
||||
tar -xvf .artifacts/zitadel-linux-amd64.tar.gz
|
||||
mv zitadel-linux-amd64/zitadel ./zitadel
|
||||
- run: make login_quality
|
||||
env:
|
||||
# latest if branch is main, otherwise image version which is the pull request number
|
||||
LOGIN_BAKE_CLI: depot bake
|
||||
DEPOT_PROJECT_ID: w47wkxzdtw
|
||||
IGNORE_RUN_CACHE: ${{ github.event.inputs.ignore-run-cache }}
|
||||
NODE_VERSION: ${{ inputs.node_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()
|
70
.github/workflows/release.yml
vendored
70
.github/workflows/release.yml
vendored
@@ -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,55 @@ jobs:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
run: |
|
||||
gh workflow -R zitadel/zitadel-charts run bump.yml
|
||||
|
||||
typescript-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
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: changesets/action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
cwd: login
|
||||
|
||||
typescript-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
|
||||
|
@@ -20,6 +20,7 @@ issues:
|
||||
- openapi
|
||||
- proto
|
||||
- tools
|
||||
- login
|
||||
|
||||
run:
|
||||
concurrency: 4
|
||||
|
@@ -18,6 +18,13 @@ 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/
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
50
Makefile
50
Makefile
@@ -12,11 +12,21 @@ 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_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/Dockerfile -t $(ZITADEL_IMAGE) .
|
||||
|
||||
.PHONY: compile_pipeline
|
||||
@@ -165,3 +175,41 @@ 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 'login' subtree on remote $(LOGIN_REMOTE_NAME) branch $(LOGIN_REMOTE_BRANCH)"
|
||||
git fetch $(LOGIN_REMOTE_NAME)
|
||||
git subtree pull --prefix=login $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_BRANCH)
|
||||
|
||||
.PHONY: login_push
|
||||
login_push: login_ensure_remote
|
||||
@echo "Pushing changes to the 'login' subtree on remote $(LOGIN_REMOTE_NAME) branch $(LOGIN_REMOTE_BRANCH)"
|
||||
git subtree push --prefix=login $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_BRANCH)
|
||||
|
||||
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
|
||||
@if [ ! -d login ]; then \
|
||||
echo "Adding subtree for 'login' from branch $(LOGIN_REMOTE_BRANCH)"; \
|
||||
git subtree add --prefix=login $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_BRANCH); \
|
||||
else \
|
||||
echo "Subtree 'login' already exists."; \
|
||||
fi
|
||||
|
||||
export LOGIN_DIR := ./login/
|
||||
export LOGIN_BAKE_CLI_ADDITIONAL_ARGS := --set login-*.context=./login/ --file ./docker-bake.hcl
|
||||
export ZITADEL_TAG ?= $(ZITADEL_IMAGE)
|
||||
include login/Makefile
|
||||
|
||||
# Intentional override of login_test_acceptance_build
|
||||
login_test_acceptance_build: docker_image
|
||||
@echo "Building login test acceptance environment with the local zitadel image"
|
||||
$(MAKE) login_test_acceptance_build_compose login_test_acceptance_build_bake
|
||||
|
||||
login_dev: docker_image typescript_generate login_test_acceptance_build_compose login_test_acceptance_cleanup login_test_acceptance_setup_dev
|
||||
@echo "Starting login test environment with the local zitadel image"
|
||||
|
3
build/Dockerfile.gitignore
Normal file
3
build/Dockerfile.gitignore
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!build/entrypoint.sh
|
||||
!zitadel
|
@@ -526,13 +526,13 @@ OIDC:
|
||||
CharSet: "BCDFGHJKLMNPQRSTVWXZ" # ZITADEL_OIDC_DEVICEAUTH_USERCODE_CHARSET
|
||||
CharAmount: 8 # ZITADEL_OIDC_DEVICEAUTH_USERCODE_CHARARMOUNT
|
||||
DashInterval: 4 # ZITADEL_OIDC_DEVICEAUTH_USERCODE_DASHINTERVAL
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
|
||||
DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
|
||||
DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
|
||||
DefaultLogoutURLV2: "/ui/v2/login/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
|
||||
PublicKeyCacheMaxAge: 24h # ZITADEL_OIDC_PUBLICKEYCACHEMAXAGE
|
||||
DefaultBackChannelLogoutLifetime: 15m # ZITADEL_OIDC_DEFAULTBACKCHANNELLOGOUTLIFETIME
|
||||
|
||||
SAML:
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||
DefaultLoginURLV2: "/ui/v2/login/login?samlRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||
ProviderConfig:
|
||||
MetadataConfig:
|
||||
Path: "/metadata" # ZITADEL_SAML_PROVIDERCONFIG_METADATACONFIG_PATH
|
||||
@@ -839,6 +839,13 @@ DefaultInstance:
|
||||
Pat:
|
||||
# date format: 2023-01-01T00:00:00Z
|
||||
ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE
|
||||
LoginClient:
|
||||
Machine:
|
||||
Username: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME
|
||||
Name: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME
|
||||
Pat:
|
||||
# date format: 2023-01-01T00:00:00Z
|
||||
ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE
|
||||
SecretGenerators:
|
||||
ClientSecret:
|
||||
Length: 64 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_CLIENTSECRET_LENGTH
|
||||
@@ -1131,8 +1138,8 @@ DefaultInstance:
|
||||
# OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION
|
||||
# DisableUserTokenEvent: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DISABLEUSERTOKENEVENT
|
||||
# EnableBackChannelLogout: false # ZITADEL_DEFAULTINSTANCE_FEATURES_ENABLEBACKCHANNELLOGOUT
|
||||
# LoginV2:
|
||||
# Required: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED
|
||||
LoginV2:
|
||||
Required: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED
|
||||
# BaseURI: "" # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI
|
||||
# PermissionCheckV2: false # ZITADEL_DEFAULTINSTANCE_FEATURES_PERMISSIONCHECKV2
|
||||
# ConsoleUseV2UserApi: false # ZITADEL_DEFAULTINSTANCE_FEATURES_CONSOLEUSEV2USERAPI
|
||||
@@ -1196,6 +1203,37 @@ DefaultInstance:
|
||||
# If an audit log retention is set using an instance limit, it will overwrite the system default.
|
||||
AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION
|
||||
|
||||
# The ServicePing are periodic reports of analytics data and the usage of ZITADEL.
|
||||
# It is sent to a central endpoint to help us improve ZITADEL.
|
||||
# It's enabled by default, but you can opt out either completely or by disabling specific telemetry data.
|
||||
ServicePing:
|
||||
# By setting Enabled to false, the service ping is disabled completely.
|
||||
Enabled: true # ZITADEL_SERVICEPING_ENABLED
|
||||
# The endpoint to which the reports are sent. The endpoint is used as a base path. Individual reports are sent to the endpoint with a specific path.
|
||||
Endpoint: "https://zitadel.cloud/api/ping" # ZITADEL_SERVICEPING_ENDPOINT
|
||||
# Interval at which the service ping is sent to the endpoint.
|
||||
# The interval is in the format of a cron expression.
|
||||
# By default, it is set to every day at midnight:
|
||||
Interval: "0 0 * * *" # ZITADEL_SERVICEPING_INTERVAL
|
||||
# Maximum number of attempts for each individual report to be sent.
|
||||
# If one report fails, it will be retried up to this number of times.
|
||||
# Other reports will still be handled in parallel and have their own retry count.
|
||||
# This means if the base information only succeeded after 3 attempts,
|
||||
# the resource count still has 5 attempts to be sent.
|
||||
MaxAttempts: 5 # ZITADEL_SERVICEPING_MAXATTEMPTS
|
||||
# The following features can be enabled or disabled individually.
|
||||
# By default, all features are enabled.
|
||||
# Note that if the service ping is enabled, base information about the system is always sent.
|
||||
# This includes the version and the id, creation date and domains of all instances.
|
||||
# If you disable a feature, it will not be sent in the service ping.
|
||||
# Some features provide additional configuration options, if enabled.
|
||||
Telemetry:
|
||||
# ResourceCount is a periodic report of the number of resources in ZITADEL.
|
||||
# This includes the number of users, organizations, projects, and other resources.
|
||||
ResourceCount:
|
||||
Enabled: true # ZITADEL_SERVICEPING_TELEMETRY_RESOURCECOUNT_ENABLED
|
||||
BulkSize: 10000 # ZITADEL_SERVICEPING_TELEMETRY_RESOURCECOUNT_BULKSIZE
|
||||
|
||||
InternalAuthZ:
|
||||
# Configure the RolePermissionMappings by environment variable using JSON notation:
|
||||
# ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write"]}]'
|
||||
|
@@ -20,12 +20,13 @@ import (
|
||||
)
|
||||
|
||||
type FirstInstance struct {
|
||||
InstanceName string
|
||||
DefaultLanguage language.Tag
|
||||
Org command.InstanceOrgSetup
|
||||
MachineKeyPath string
|
||||
PatPath string
|
||||
Features *command.InstanceFeatures
|
||||
InstanceName string
|
||||
DefaultLanguage language.Tag
|
||||
Org command.InstanceOrgSetup
|
||||
MachineKeyPath string
|
||||
PatPath string
|
||||
LoginClientPatPath string
|
||||
Features *command.InstanceFeatures
|
||||
|
||||
Skip bool
|
||||
|
||||
@@ -121,16 +122,18 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
||||
}
|
||||
}
|
||||
|
||||
_, token, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
|
||||
_, token, key, loginClientToken, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mig.instanceSetup.Org.Machine != nil &&
|
||||
if (mig.instanceSetup.Org.Machine != nil &&
|
||||
((mig.instanceSetup.Org.Machine.Pat != nil && token == "") ||
|
||||
(mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil)) {
|
||||
(mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil))) ||
|
||||
(mig.instanceSetup.Org.LoginClient != nil &&
|
||||
(mig.instanceSetup.Org.LoginClient.Pat != nil && loginClientToken == "")) {
|
||||
return err
|
||||
}
|
||||
return mig.outputMachineAuthentication(key, token)
|
||||
return mig.outputMachineAuthentication(key, token, loginClientToken)
|
||||
}
|
||||
|
||||
func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.Database, error) {
|
||||
@@ -150,7 +153,7 @@ func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.
|
||||
return keyStorage, nil
|
||||
}
|
||||
|
||||
func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token string) error {
|
||||
func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token, loginClientToken string) error {
|
||||
if key != nil {
|
||||
keyDetails, err := key.Detail()
|
||||
if err != nil {
|
||||
@@ -165,6 +168,11 @@ func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, t
|
||||
return err
|
||||
}
|
||||
}
|
||||
if loginClientToken != "" {
|
||||
if err := outputStdoutOrPath(mig.LoginClientPatPath, loginClientToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
27
cmd/setup/60.go
Normal file
27
cmd/setup/60.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/serviceping"
|
||||
"github.com/zitadel/zitadel/internal/v2/system"
|
||||
)
|
||||
|
||||
type GenerateSystemID struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
|
||||
func (mig *GenerateSystemID) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
id, err := serviceping.GenerateSystemID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mig.eventstore.Push(ctx, system.NewIDGeneratedEvent(ctx, id))
|
||||
return err
|
||||
}
|
||||
|
||||
func (mig *GenerateSystemID) String() string {
|
||||
return "60_generate_system_id"
|
||||
}
|
@@ -156,6 +156,7 @@ type Steps struct {
|
||||
s57CreateResourceCounts *CreateResourceCounts
|
||||
s58ReplaceLoginNames3View *ReplaceLoginNames3View
|
||||
s59SetupWebkeys *SetupWebkeys
|
||||
s60GenerateSystemID *GenerateSystemID
|
||||
s61AddUIDniqueConstraintsForOrgs *AddIDUniqueConstraintsForOrgs
|
||||
}
|
||||
|
||||
|
@@ -217,6 +217,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient}
|
||||
steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient}
|
||||
steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient}
|
||||
steps.s60GenerateSystemID = &GenerateSystemID{eventstore: eventstoreClient}
|
||||
steps.s61AddUIDniqueConstraintsForOrgs = &AddIDUniqueConstraintsForOrgs{eventstore: eventstoreClient, dbClient: dbClient}
|
||||
|
||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
@@ -265,6 +266,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s56IDPTemplate6SAMLFederatedLogout,
|
||||
steps.s57CreateResourceCounts,
|
||||
steps.s58ReplaceLoginNames3View,
|
||||
steps.s60GenerateSystemID,
|
||||
steps.s61AddUIDniqueConstraintsForOrgs,
|
||||
} {
|
||||
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
|
@@ -6,6 +6,7 @@ FirstInstance:
|
||||
MachineKeyPath: # ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH
|
||||
# The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath.
|
||||
PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH
|
||||
LoginClientPatPath: # ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH
|
||||
InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME
|
||||
DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE
|
||||
Org:
|
||||
@@ -46,6 +47,13 @@ FirstInstance:
|
||||
Pat:
|
||||
# date format: 2023-01-01T00:00:00Z
|
||||
ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE
|
||||
LoginClient:
|
||||
Machine:
|
||||
Username: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME
|
||||
Name: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME
|
||||
Pat:
|
||||
# date format: 2023-01-01T00:00:00Z
|
||||
ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE
|
||||
|
||||
CorrectCreationDate:
|
||||
FailAfter: 5m # ZITADEL_CORRECTCREATIONDATE_FAILAFTER
|
||||
|
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/notification/handlers"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/serviceping"
|
||||
static_config "github.com/zitadel/zitadel/internal/static/config"
|
||||
metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config"
|
||||
profiler "github.com/zitadel/zitadel/internal/telemetry/profiler/config"
|
||||
@@ -81,6 +82,7 @@ type Config struct {
|
||||
LogStore *logstore.Configs
|
||||
Quotas *QuotasConfig
|
||||
Telemetry *handlers.TelemetryPusherConfig
|
||||
ServicePing *serviceping.Config
|
||||
}
|
||||
|
||||
type QuotasConfig struct {
|
||||
|
@@ -99,6 +99,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/notification"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/queue"
|
||||
"github.com/zitadel/zitadel/internal/serviceping"
|
||||
"github.com/zitadel/zitadel/internal/static"
|
||||
es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres"
|
||||
@@ -317,10 +318,20 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
)
|
||||
execution.Start(ctx)
|
||||
|
||||
// the service ping and it's workers need to be registered before starting the queue
|
||||
if err := serviceping.Register(ctx, q, queries, eventstoreClient, config.ServicePing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = q.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// the scheduler / periodic jobs need to be started after the queue already runs
|
||||
if err = serviceping.Start(config.ServicePing, q); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
tlsConfig, err := config.TLS.Config()
|
||||
if err != nil {
|
||||
|
5
docker-bake.hcl
Normal file
5
docker-bake.hcl
Normal file
@@ -0,0 +1,5 @@
|
||||
target "typescript-proto-client" {
|
||||
contexts = {
|
||||
proto-files = "target:proto-files"
|
||||
}
|
||||
}
|
8
dockerfiles/proto-files.Dockerfile
Normal file
8
dockerfiles/proto-files.Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
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 google/api/annotations.proto --path google/api/http.proto --path google/api/field_behavior.proto --output /proto-files
|
||||
|
||||
FROM scratch
|
||||
COPY --from=proto-files /proto-files /
|
||||
COPY ./proto /
|
2
dockerfiles/proto-files.Dockerfile.dockerignore
Normal file
2
dockerfiles/proto-files.Dockerfile.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!proto
|
8
dockerfiles/typescript-proto-client.Dockerfile
Normal file
8
dockerfiles/typescript-proto-client.Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM login-pnpm AS typescript-proto-client
|
||||
COPY ./login/packages/zitadel-proto/package.json ./packages/zitadel-proto/
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile --workspace-root --filter zitadel-proto
|
||||
COPY --from=proto-files /buf.yaml /buf.lock /proto-files/
|
||||
COPY --from=proto-files /zitadel /proto-files/zitadel
|
||||
COPY ./login/packages/zitadel-proto/buf.gen.yaml ./packages/zitadel-proto/
|
||||
RUN cd packages/zitadel-proto && pnpm exec buf generate /proto-files
|
11
dockerfiles/typescript-proto-client.Dockerfile.dockerignore
Normal file
11
dockerfiles/typescript-proto-client.Dockerfile.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
*
|
||||
!/login/packages/zitadel-proto/
|
||||
login/packages/zitadel-proto/google
|
||||
login/packages/zitadel-proto/zitadel
|
||||
login/packages/zitadel-proto/protoc-gen-openapiv2
|
||||
login/packages/zitadel-proto/validate
|
||||
|
||||
**/*.md
|
||||
**/*.png
|
||||
**/node_modules
|
||||
**/.turbo
|
@@ -1 +1 @@
|
||||
.env-file
|
||||
.env-file
|
@@ -41,17 +41,17 @@ services:
|
||||
user: root
|
||||
entrypoint: '/bin/sh'
|
||||
command:
|
||||
- -c
|
||||
- >
|
||||
/app/zitadel setup
|
||||
--config /example-zitadel-config.yaml
|
||||
--config /example-zitadel-secrets.yaml
|
||||
--steps /example-zitadel-init-steps.yaml
|
||||
--masterkey ${ZITADEL_MASTERKEY} &&
|
||||
mv /pat /.env-file/pat || exit 0 &&
|
||||
echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env &&
|
||||
chown -R 1001:${GID} /.env-file &&
|
||||
chmod -R 770 /.env-file
|
||||
- -c
|
||||
- >
|
||||
/app/zitadel setup
|
||||
--config /example-zitadel-config.yaml
|
||||
--config /example-zitadel-secrets.yaml
|
||||
--steps /example-zitadel-init-steps.yaml
|
||||
--masterkey ${ZITADEL_MASTERKEY} &&
|
||||
mv /pat /.env-file/pat || exit 0 &&
|
||||
echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env &&
|
||||
chown -R 1001:${GID} /.env-file &&
|
||||
chmod -R 770 /.env-file
|
||||
environment:
|
||||
- GID
|
||||
depends_on:
|
||||
@@ -154,4 +154,4 @@ networks:
|
||||
backend:
|
||||
|
||||
volumes:
|
||||
data:
|
||||
data:
|
@@ -26,4 +26,4 @@ SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULT
|
||||
LogStore.Access.Stdout.Enabled: true
|
||||
|
||||
# Skipping the MFA init step allows us to immediately authenticate at the console
|
||||
DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s"
|
||||
DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s"
|
@@ -9,4 +9,4 @@ FirstInstance:
|
||||
Machine:
|
||||
Username: 'login-container'
|
||||
Name: 'Login Container'
|
||||
Pat.ExpirationDate: '2029-01-01T00:00:00Z'
|
||||
Pat.ExpirationDate: '2029-01-01T00:00:00Z'
|
@@ -71,4 +71,4 @@ Open your favorite internet browser at https://127.0.0.1.sslip.io/ui/console?log
|
||||
Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed.
|
||||
Use the password *Password1!* to log in.
|
||||
|
||||
Read more about [the login process](/guides/integrate/login/oidc/login-users).
|
||||
Read more about [the login process](/guides/integrate/login/oidc/login-users).
|
@@ -64,4 +64,4 @@ mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
|
||||
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
|
||||
|
||||
<Next components={props.components} />
|
||||
<Disclaimer components={props.components} />
|
||||
<Disclaimer components={props.components} />
|
@@ -60,6 +60,9 @@ Projections:
|
||||
DefaultInstance:
|
||||
LoginPolicy:
|
||||
MfaInitSkipLifetime: "0"
|
||||
Features:
|
||||
LoginV2:
|
||||
Required: false
|
||||
|
||||
SystemAPIUsers:
|
||||
- cypress:
|
||||
|
@@ -52,6 +52,9 @@ Quotas:
|
||||
DefaultInstance:
|
||||
LoginPolicy:
|
||||
MfaInitSkipLifetime: "0"
|
||||
Features:
|
||||
LoginV2:
|
||||
Required: false
|
||||
|
||||
SystemAPIUsers:
|
||||
- cypress:
|
||||
|
1
go.mod
1
go.mod
@@ -67,6 +67,7 @@ require (
|
||||
github.com/riverqueue/river/riverdriver v0.22.0
|
||||
github.com/riverqueue/river/rivertype v0.22.0
|
||||
github.com/riverqueue/rivercontrib/otelriver v0.5.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
|
@@ -40,7 +40,7 @@ func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequ
|
||||
}
|
||||
|
||||
func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequest) (*system_pb.AddInstanceResponse, error) {
|
||||
id, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
|
||||
id, _, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (s *Server) UpdateInstance(ctx context.Context, req *system_pb.UpdateInstan
|
||||
}
|
||||
|
||||
func (s *Server) CreateInstance(ctx context.Context, req *system_pb.CreateInstanceRequest) (*system_pb.CreateInstanceResponse, error) {
|
||||
id, pat, key, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
|
||||
id, pat, key, _, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -217,33 +217,33 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, *domain.ObjectDetails, error) {
|
||||
func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, string, *domain.ObjectDetails, error) {
|
||||
if err := setup.generateIDs(c.idGenerator); err != nil {
|
||||
return "", "", nil, nil, err
|
||||
return "", "", nil, "", nil, err
|
||||
}
|
||||
ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage)
|
||||
|
||||
validations, pat, machineKey, err := setUpInstance(ctx, c, setup)
|
||||
validations, pat, machineKey, loginClientPat, err := setUpInstance(ctx, c, setup)
|
||||
if err != nil {
|
||||
return "", "", nil, nil, err
|
||||
return "", "", nil, "", nil, err
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
||||
if err != nil {
|
||||
return "", "", nil, nil, err
|
||||
return "", "", nil, "", nil, err
|
||||
}
|
||||
|
||||
_, err = c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return "", "", nil, nil, err
|
||||
return "", "", nil, "", nil, err
|
||||
}
|
||||
|
||||
// RolePermissions need to be pushed in separate transaction.
|
||||
// https://github.com/zitadel/zitadel/issues/9293
|
||||
details, err := c.SynchronizeRolePermission(ctx, setup.zitadel.instanceID, setup.RolePermissionMappings)
|
||||
if err != nil {
|
||||
return "", "", nil, nil, err
|
||||
return "", "", nil, "", nil, err
|
||||
}
|
||||
details.ResourceOwner = setup.zitadel.orgID
|
||||
|
||||
@@ -251,8 +251,12 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
if pat != nil {
|
||||
token = pat.Token
|
||||
}
|
||||
var loginClientToken string
|
||||
if loginClientPat != nil {
|
||||
loginClientToken = loginClientPat.Token
|
||||
}
|
||||
|
||||
return setup.zitadel.instanceID, token, machineKey, details, nil
|
||||
return setup.zitadel.instanceID, token, machineKey, loginClientToken, details, nil
|
||||
}
|
||||
|
||||
func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context {
|
||||
@@ -274,38 +278,38 @@ func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, co
|
||||
)
|
||||
}
|
||||
|
||||
func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, err error) {
|
||||
func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) {
|
||||
instanceAgg := instance.NewAggregate(setup.zitadel.instanceID)
|
||||
|
||||
validations = setupInstanceElements(instanceAgg, setup)
|
||||
|
||||
// default organization on setup'd instance
|
||||
pat, machineKey, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.zitadel)
|
||||
pat, machineKey, loginClientPat, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.Org.LoginClient, setup.zitadel)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
// domains
|
||||
if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
||||
|
||||
// optional setting if set
|
||||
setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
|
||||
if err := setupQuotas(c, &validations, setup.Quotas, setup.zitadel.instanceID); err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
|
||||
if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
|
||||
setupFeatures(&validations, setup.Features, setup.zitadel.instanceID)
|
||||
setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits)
|
||||
setupRestrictions(c, &validations, restrictions.NewAggregate(setup.zitadel.restrictionsID, setup.zitadel.instanceID, setup.zitadel.instanceID), setup.Restrictions)
|
||||
setupInstanceCreatedMilestone(&validations, setup.zitadel.instanceID)
|
||||
return validations, pat, machineKey, nil
|
||||
return validations, pat, machineKey, loginClientPat, nil
|
||||
}
|
||||
|
||||
func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup) []preparation.Validation {
|
||||
@@ -572,8 +576,9 @@ func setupDefaultOrg(ctx context.Context,
|
||||
name string,
|
||||
machine *AddMachine,
|
||||
human *AddHuman,
|
||||
loginClient *AddLoginClient,
|
||||
ids ZitadelConfig,
|
||||
) (pat *PersonalAccessToken, machineKey *MachineKey, err error) {
|
||||
) (pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) {
|
||||
orgAgg := org.NewAggregate(ids.orgID)
|
||||
|
||||
*validations = append(
|
||||
@@ -582,12 +587,12 @@ func setupDefaultOrg(ctx context.Context,
|
||||
commands.prepareSetDefaultOrg(instanceAgg, ids.orgID),
|
||||
)
|
||||
|
||||
projectOwner, pat, machineKey, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human)
|
||||
projectOwner, pat, machineKey, loginClientPat, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human, loginClient)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids)
|
||||
return pat, machineKey, nil
|
||||
return pat, machineKey, loginClientPat, nil
|
||||
}
|
||||
|
||||
func setupAdmins(commands *Commands,
|
||||
@@ -596,21 +601,22 @@ func setupAdmins(commands *Commands,
|
||||
orgAgg *org.Aggregate,
|
||||
machine *AddMachine,
|
||||
human *AddHuman,
|
||||
) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, err error) {
|
||||
loginClient *AddLoginClient,
|
||||
) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) {
|
||||
if human == nil && machine == nil {
|
||||
return "", nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin")
|
||||
return "", nil, nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin")
|
||||
}
|
||||
|
||||
if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() {
|
||||
machineUserID, err := commands.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
owner = machineUserID
|
||||
|
||||
pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
|
||||
setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID)
|
||||
@@ -618,7 +624,7 @@ func setupAdmins(commands *Commands,
|
||||
if human != nil {
|
||||
humanUserID, err := commands.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
owner = humanUserID
|
||||
human.ID = humanUserID
|
||||
@@ -629,7 +635,18 @@ func setupAdmins(commands *Commands,
|
||||
|
||||
setupAdminMembers(commands, validations, instanceAgg, orgAgg, humanUserID)
|
||||
}
|
||||
return owner, pat, machineKey, nil
|
||||
if loginClient != nil {
|
||||
loginClientUserID, err := commands.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
|
||||
loginClientPat, err = setupLoginClient(commands, validations, instanceAgg, loginClient, orgAgg.ID, loginClientUserID)
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
return owner, pat, machineKey, loginClientPat, nil
|
||||
}
|
||||
|
||||
func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, orgID, userID string) (pat *PersonalAccessToken, machineKey *MachineKey, err error) {
|
||||
@@ -655,6 +672,22 @@ func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation
|
||||
return pat, machineKey, nil
|
||||
}
|
||||
|
||||
func setupLoginClient(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, loginClient *AddLoginClient, orgID, userID string) (pat *PersonalAccessToken, err error) {
|
||||
*validations = append(*validations,
|
||||
AddMachineCommand(user.NewAggregate(userID, orgID), loginClient.Machine),
|
||||
commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMLoginClient),
|
||||
)
|
||||
if loginClient.Pat != nil {
|
||||
pat = NewPersonalAccessToken(orgID, userID, loginClient.Pat.ExpirationDate, loginClient.Pat.Scopes, domain.UserTypeMachine)
|
||||
pat.TokenID, err = commands.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm))
|
||||
}
|
||||
return pat, nil
|
||||
}
|
||||
|
||||
func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) {
|
||||
*validations = append(*validations,
|
||||
commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),
|
||||
|
@@ -129,7 +129,7 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str
|
||||
}
|
||||
}
|
||||
|
||||
func orgFilters(orgID string, machine, human bool) []expect {
|
||||
func orgFilters(orgID string, machine, human, loginClient bool) []expect {
|
||||
filters := []expect{
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
@@ -144,13 +144,17 @@ func orgFilters(orgID string, machine, human bool) []expect {
|
||||
filters = append(filters, humanFilters(orgID)...)
|
||||
filters = append(filters, adminMemberFilters(orgID, "USER")...)
|
||||
}
|
||||
if loginClient {
|
||||
filters = append(filters, loginClientFilters(orgID, true)...)
|
||||
filters = append(filters, instanceMemberFilters(orgID, "USER-LOGIN-CLIENT")...)
|
||||
}
|
||||
|
||||
return append(filters,
|
||||
projectFilters()...,
|
||||
)
|
||||
}
|
||||
|
||||
func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human bool) []eventstore.Command {
|
||||
func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human, loginClient bool) []eventstore.Command {
|
||||
instanceAgg := instance.NewAggregate(instanceID)
|
||||
orgAgg := org.NewAggregate(orgID)
|
||||
domain := strings.ToLower(name + "." + defaultDomain)
|
||||
@@ -173,13 +177,17 @@ func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultD
|
||||
events = append(events, humanEvents(ctx, instanceID, orgID, userID)...)
|
||||
owner = userID
|
||||
}
|
||||
if loginClient {
|
||||
userID := "USER-LOGIN-CLIENT"
|
||||
events = append(events, loginClientEvents(ctx, instanceID, orgID, userID, "LOGIN-CLIENT-PAT")...)
|
||||
}
|
||||
|
||||
events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...)
|
||||
return events
|
||||
}
|
||||
|
||||
func orgIDs() []string {
|
||||
return slices.Concat([]string{"USER-MACHINE", "PAT", "USER"}, projectClientIDs())
|
||||
return slices.Concat([]string{"USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"}, projectClientIDs())
|
||||
}
|
||||
|
||||
func instancePoliciesFilters(instanceID string) []expect {
|
||||
@@ -363,7 +371,7 @@ func instanceElementsConfig() *SecretGenerators {
|
||||
func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect {
|
||||
return slices.Concat(
|
||||
setupInstanceElementsFilters(instanceID),
|
||||
orgFilters(orgID, true, true),
|
||||
orgFilters(orgID, true, true, true),
|
||||
generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
|
||||
)
|
||||
}
|
||||
@@ -371,7 +379,7 @@ func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []
|
||||
func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command {
|
||||
return slices.Concat(
|
||||
setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
|
||||
orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true),
|
||||
orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true, true),
|
||||
generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
|
||||
instanceCreatedMilestoneEvent(ctx, instanceID),
|
||||
)
|
||||
@@ -380,9 +388,10 @@ func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appI
|
||||
func setupInstanceConfig() *InstanceSetup {
|
||||
conf := setupInstanceElementsConfig()
|
||||
conf.Org = InstanceOrgSetup{
|
||||
Name: "ZITADEL",
|
||||
Machine: instanceSetupMachineConfig(),
|
||||
Human: instanceSetupHumanConfig(),
|
||||
Name: "ZITADEL",
|
||||
Machine: instanceSetupMachineConfig(),
|
||||
Human: instanceSetupHumanConfig(),
|
||||
LoginClient: instanceSetupLoginClientConfig(),
|
||||
}
|
||||
conf.CustomDomain = ""
|
||||
return conf
|
||||
@@ -541,6 +550,43 @@ func instanceSetupMachineConfig() *AddMachine {
|
||||
}
|
||||
}
|
||||
|
||||
func loginClientFilters(orgID string, pat bool) []expect {
|
||||
filters := []expect{
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
org.NewDomainPolicyAddedEvent(
|
||||
context.Background(),
|
||||
&org.NewAggregate(orgID).Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
}
|
||||
if pat {
|
||||
filters = append(filters,
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
)
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
func instanceSetupLoginClientConfig() *AddLoginClient {
|
||||
return &AddLoginClient{
|
||||
Machine: &Machine{
|
||||
Username: "zitadel-login-client",
|
||||
Name: "ZITADEL-login-client",
|
||||
Description: "Login Client",
|
||||
AccessTokenType: domain.OIDCTokenTypeBearer,
|
||||
},
|
||||
Pat: &AddPat{
|
||||
ExpirationDate: time.Time{},
|
||||
Scopes: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func projectFilters() []expect {
|
||||
return []expect{
|
||||
expectFilter(),
|
||||
@@ -551,11 +597,23 @@ func projectFilters() []expect {
|
||||
}
|
||||
|
||||
func adminMemberFilters(orgID, userID string) []expect {
|
||||
filters := append(
|
||||
orgMemberFilters(orgID, userID),
|
||||
instanceMemberFilters(orgID, userID)...,
|
||||
)
|
||||
return filters
|
||||
}
|
||||
func orgMemberFilters(orgID, userID string) []expect {
|
||||
return []expect{
|
||||
expectFilter(
|
||||
addHumanEvent(context.Background(), orgID, userID),
|
||||
),
|
||||
expectFilter(),
|
||||
}
|
||||
}
|
||||
|
||||
func instanceMemberFilters(orgID, userID string) []expect {
|
||||
return []expect{
|
||||
expectFilter(
|
||||
addHumanEvent(context.Background(), orgID, userID),
|
||||
),
|
||||
@@ -631,6 +689,40 @@ func addMachineEvent(ctx context.Context, orgID, userID string) *user.MachineAdd
|
||||
)
|
||||
}
|
||||
|
||||
// loginClientEvents all events from setup to create the login client user
|
||||
func loginClientEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command {
|
||||
agg := user.NewAggregate(userID, orgID)
|
||||
instanceAgg := instance.NewAggregate(instanceID)
|
||||
events := []eventstore.Command{
|
||||
addLoginClientEvent(ctx, orgID, userID),
|
||||
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMLoginClient),
|
||||
}
|
||||
if patID != "" {
|
||||
events = append(events,
|
||||
user.NewPersonalAccessTokenAddedEvent(
|
||||
ctx,
|
||||
&agg.Aggregate,
|
||||
patID,
|
||||
time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC),
|
||||
nil,
|
||||
),
|
||||
)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func addLoginClientEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent {
|
||||
agg := user.NewAggregate(userID, orgID)
|
||||
return user.NewMachineAddedEvent(ctx,
|
||||
&agg.Aggregate,
|
||||
"zitadel-login-client",
|
||||
"ZITADEL-login-client",
|
||||
"Login Client",
|
||||
false,
|
||||
domain.OIDCTokenTypeBearer,
|
||||
)
|
||||
}
|
||||
|
||||
func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error {
|
||||
//nolint:staticcheck
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
||||
@@ -715,6 +807,13 @@ func TestCommandSide_setupMinimalInterfaces(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func validZitadelRoles() []authz.RoleMapping {
|
||||
return []authz.RoleMapping{
|
||||
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMLoginClient, Permissions: []string{""}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
type fields struct {
|
||||
@@ -730,12 +829,14 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
orgAgg *org.Aggregate
|
||||
machine *AddMachine
|
||||
human *AddHuman
|
||||
loginClient *AddLoginClient
|
||||
}
|
||||
type res struct {
|
||||
owner string
|
||||
pat bool
|
||||
machineKey bool
|
||||
err func(error) bool
|
||||
owner string
|
||||
pat bool
|
||||
machineKey bool
|
||||
loginClientPat bool
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -763,10 +864,7 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
roles: []authz.RoleMapping{
|
||||
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
|
||||
},
|
||||
roles: validZitadelRoles(),
|
||||
},
|
||||
args: args{
|
||||
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
|
||||
@@ -800,11 +898,8 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
},
|
||||
)...,
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"),
|
||||
roles: []authz.RoleMapping{
|
||||
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
|
||||
},
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"),
|
||||
roles: validZitadelRoles(),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
@@ -850,11 +945,8 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"),
|
||||
roles: []authz.RoleMapping{
|
||||
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
|
||||
},
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
roles: validZitadelRoles(),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
|
||||
@@ -870,6 +962,63 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "human, machine and login client, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
slices.Concat(
|
||||
machineFilters("ORG", true),
|
||||
adminMemberFilters("ORG", "USER-MACHINE"),
|
||||
humanFilters("ORG"),
|
||||
adminMemberFilters("ORG", "USER"),
|
||||
loginClientFilters("ORG", true),
|
||||
instanceMemberFilters("ORG", "USER-LOGIN-CLIENT"),
|
||||
[]expect{
|
||||
expectPush(
|
||||
slices.Concat(
|
||||
machineEvents(context.Background(),
|
||||
"INSTANCE",
|
||||
"ORG",
|
||||
"USER-MACHINE",
|
||||
"PAT",
|
||||
),
|
||||
humanEvents(context.Background(),
|
||||
"INSTANCE",
|
||||
"ORG",
|
||||
"USER",
|
||||
),
|
||||
loginClientEvents(context.Background(),
|
||||
"INSTANCE",
|
||||
"ORG",
|
||||
"USER-LOGIN-CLIENT",
|
||||
"LOGIN-CLIENT-PAT",
|
||||
),
|
||||
)...,
|
||||
),
|
||||
},
|
||||
)...,
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"),
|
||||
roles: validZitadelRoles(),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
|
||||
instanceAgg: instance.NewAggregate("INSTANCE"),
|
||||
orgAgg: org.NewAggregate("ORG"),
|
||||
machine: instanceSetupMachineConfig(),
|
||||
human: instanceSetupHumanConfig(),
|
||||
loginClient: instanceSetupLoginClientConfig(),
|
||||
},
|
||||
res: res{
|
||||
owner: "USER",
|
||||
pat: true,
|
||||
machineKey: false,
|
||||
loginClientPat: true,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -881,7 +1030,7 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
keyAlgorithm: tt.fields.keyAlgorithm,
|
||||
}
|
||||
validations := make([]preparation.Validation, 0)
|
||||
owner, pat, mk, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human)
|
||||
owner, pat, mk, loginClientPat, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human, tt.args.loginClient)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -905,6 +1054,9 @@ func TestCommandSide_setupAdmins(t *testing.T) {
|
||||
if tt.res.machineKey {
|
||||
assert.NotNil(t, mk)
|
||||
}
|
||||
if tt.res.loginClientPat {
|
||||
assert.NotNil(t, loginClientPat)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -924,12 +1076,14 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
orgName string
|
||||
machine *AddMachine
|
||||
human *AddHuman
|
||||
loginClient *AddLoginClient
|
||||
ids ZitadelConfig
|
||||
}
|
||||
type res struct {
|
||||
pat bool
|
||||
machineKey bool
|
||||
err func(error) bool
|
||||
pat bool
|
||||
machineKey bool
|
||||
loginClientPat bool
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -938,7 +1092,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "human and machine, ok",
|
||||
name: "human, machine and login client, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
slices.Concat(
|
||||
@@ -946,6 +1100,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
"ORG",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
[]expect{
|
||||
expectPush(
|
||||
@@ -959,6 +1114,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
)...,
|
||||
),
|
||||
@@ -967,11 +1123,8 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
|
||||
roles: []authz.RoleMapping{
|
||||
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
|
||||
},
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
roles: validZitadelRoles(),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
|
||||
@@ -1007,6 +1160,18 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
Password: "password",
|
||||
PasswordChangeRequired: false,
|
||||
},
|
||||
loginClient: &AddLoginClient{
|
||||
Machine: &Machine{
|
||||
Username: "zitadel-login-client",
|
||||
Name: "ZITADEL-login-client",
|
||||
Description: "Login Client",
|
||||
AccessTokenType: domain.OIDCTokenTypeBearer,
|
||||
},
|
||||
Pat: &AddPat{
|
||||
ExpirationDate: time.Time{},
|
||||
Scopes: nil,
|
||||
},
|
||||
},
|
||||
ids: ZitadelConfig{
|
||||
instanceID: "INSTANCE",
|
||||
orgID: "ORG",
|
||||
@@ -1018,9 +1183,10 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
pat: true,
|
||||
machineKey: false,
|
||||
err: nil,
|
||||
pat: true,
|
||||
machineKey: false,
|
||||
loginClientPat: true,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1034,7 +1200,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
keyAlgorithm: tt.fields.keyAlgorithm,
|
||||
}
|
||||
validations := make([]preparation.Validation, 0)
|
||||
pat, mk, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.ids)
|
||||
pat, mk, loginClientPat, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.loginClient, tt.args.ids)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1057,6 +1223,9 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) {
|
||||
if tt.res.machineKey {
|
||||
assert.NotNil(t, mk)
|
||||
}
|
||||
if tt.res.loginClientPat {
|
||||
assert.NotNil(t, loginClientPat)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1140,9 +1309,10 @@ func TestCommandSide_setUpInstance(t *testing.T) {
|
||||
setup *InstanceSetup
|
||||
}
|
||||
type res struct {
|
||||
pat bool
|
||||
machineKey bool
|
||||
err func(error) bool
|
||||
pat bool
|
||||
machineKey bool
|
||||
loginClientPat bool
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1175,11 +1345,8 @@ func TestCommandSide_setUpInstance(t *testing.T) {
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
|
||||
roles: []authz.RoleMapping{
|
||||
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
|
||||
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
|
||||
},
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
roles: validZitadelRoles(),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
generateDomain: func(string, string) (string, error) {
|
||||
return "DOMAIN", nil
|
||||
},
|
||||
@@ -1204,7 +1371,7 @@ func TestCommandSide_setUpInstance(t *testing.T) {
|
||||
GenerateDomain: tt.fields.generateDomain,
|
||||
}
|
||||
|
||||
validations, pat, mk, err := setUpInstance(tt.args.ctx, r, tt.args.setup)
|
||||
validations, pat, mk, loginClientPat, err := setUpInstance(tt.args.ctx, r, tt.args.setup)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1227,6 +1394,9 @@ func TestCommandSide_setUpInstance(t *testing.T) {
|
||||
if tt.res.machineKey {
|
||||
assert.NotNil(t, mk)
|
||||
}
|
||||
if tt.res.loginClientPat {
|
||||
assert.NotNil(t, loginClientPat)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -24,9 +24,15 @@ type InstanceOrgSetup struct {
|
||||
CustomDomain string
|
||||
Human *AddHuman
|
||||
Machine *AddMachine
|
||||
LoginClient *AddLoginClient
|
||||
Roles []string
|
||||
}
|
||||
|
||||
type AddLoginClient struct {
|
||||
Machine *Machine
|
||||
Pat *AddPat
|
||||
}
|
||||
|
||||
type OrgSetup struct {
|
||||
Name string
|
||||
CustomDomain string
|
||||
|
@@ -14,6 +14,7 @@ const (
|
||||
RoleOrgOwner = "ORG_OWNER"
|
||||
RoleOrgProjectCreator = "ORG_PROJECT_CREATOR"
|
||||
RoleIAMOwner = "IAM_OWNER"
|
||||
RoleIAMLoginClient = "IAM_LOGIN_CLIENT"
|
||||
RoleProjectOwner = "PROJECT_OWNER"
|
||||
RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL"
|
||||
RoleProjectGrantOwner = "PROJECT_GRANT_OWNER"
|
||||
|
@@ -101,3 +101,10 @@ SystemDefaults:
|
||||
KeyConfig:
|
||||
PrivateKeyLifetime: 7200h
|
||||
PublicKeyLifetime: 14400h
|
||||
|
||||
OIDC:
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
|
||||
DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
|
||||
|
||||
SAML:
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/riverqueue/river/riverdriver/riverpgxv5"
|
||||
"github.com/riverqueue/river/rivertype"
|
||||
"github.com/riverqueue/rivercontrib/otelriver"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
@@ -75,6 +76,26 @@ func (q *Queue) AddWorkers(w ...Worker) {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) AddPeriodicJob(schedule cron.Schedule, jobArgs river.JobArgs, opts ...InsertOpt) (handle rivertype.PeriodicJobHandle) {
|
||||
if q == nil {
|
||||
logging.Info("skip adding periodic job because queue is not set")
|
||||
return
|
||||
}
|
||||
options := new(river.InsertOpts)
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
return q.client.PeriodicJobs().Add(
|
||||
river.NewPeriodicJob(
|
||||
schedule,
|
||||
func() (river.JobArgs, *river.InsertOpts) {
|
||||
return jobArgs, options
|
||||
},
|
||||
nil,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type InsertOpt func(*river.InsertOpts)
|
||||
|
||||
func WithMaxAttempts(maxAttempts uint8) InsertOpt {
|
||||
|
153
internal/serviceping/client.go
Normal file
153
internal/serviceping/client.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package serviceping
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
|
||||
)
|
||||
|
||||
const (
|
||||
pathBaseInformation = "/instances"
|
||||
pathResourceCounts = "/resource_counts"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func (c Client) ReportBaseInformation(ctx context.Context, in *analytics.ReportBaseInformationRequest, opts ...grpc.CallOption) (*analytics.ReportBaseInformationResponse, error) {
|
||||
reportResponse := new(analytics.ReportBaseInformationResponse)
|
||||
err := c.callTelemetryService(ctx, pathBaseInformation, in, reportResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reportResponse, nil
|
||||
}
|
||||
|
||||
func (c Client) ReportResourceCounts(ctx context.Context, in *analytics.ReportResourceCountsRequest, opts ...grpc.CallOption) (*analytics.ReportResourceCountsResponse, error) {
|
||||
reportResponse := new(analytics.ReportResourceCountsResponse)
|
||||
err := c.callTelemetryService(ctx, pathResourceCounts, in, reportResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reportResponse, nil
|
||||
}
|
||||
|
||||
func (c Client) callTelemetryService(ctx context.Context, path string, in proto.Message, out proto.Message) error {
|
||||
requestBody, err := protojson.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint+path, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return &TelemetryError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
return protojson.UnmarshalOptions{
|
||||
AllowPartial: true,
|
||||
DiscardUnknown: true,
|
||||
}.Unmarshal(body, out)
|
||||
}
|
||||
|
||||
func NewClient(config *Config) Client {
|
||||
return Client{
|
||||
httpClient: http.DefaultClient,
|
||||
endpoint: config.Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateSystemID() (string, error) {
|
||||
randBytes := make([]byte, 64)
|
||||
if _, err := rand.Read(randBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(randBytes), nil
|
||||
}
|
||||
|
||||
func instanceInformationToPb(instances *query.Instances) []*analytics.InstanceInformation {
|
||||
instanceInformation := make([]*analytics.InstanceInformation, len(instances.Instances))
|
||||
for i, instance := range instances.Instances {
|
||||
domains := instanceDomainToPb(instance)
|
||||
instanceInformation[i] = &analytics.InstanceInformation{
|
||||
Id: instance.ID,
|
||||
Domains: domains,
|
||||
CreatedAt: timestamppb.New(instance.CreationDate),
|
||||
}
|
||||
}
|
||||
return instanceInformation
|
||||
}
|
||||
|
||||
func instanceDomainToPb(instance *query.Instance) []string {
|
||||
domains := make([]string, len(instance.Domains))
|
||||
for i, domain := range instance.Domains {
|
||||
domains[i] = domain.Domain
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
func resourceCountsToPb(counts []query.ResourceCount) []*analytics.ResourceCount {
|
||||
resourceCounts := make([]*analytics.ResourceCount, len(counts))
|
||||
for i, count := range counts {
|
||||
resourceCounts[i] = &analytics.ResourceCount{
|
||||
InstanceId: count.InstanceID,
|
||||
ParentType: countParentTypeToPb(count.ParentType),
|
||||
ParentId: count.ParentID,
|
||||
ResourceName: count.Resource,
|
||||
TableName: count.TableName,
|
||||
UpdatedAt: timestamppb.New(count.UpdatedAt),
|
||||
Amount: uint32(count.Amount),
|
||||
}
|
||||
}
|
||||
return resourceCounts
|
||||
}
|
||||
|
||||
func countParentTypeToPb(parentType domain.CountParentType) analytics.CountParentType {
|
||||
switch parentType {
|
||||
case domain.CountParentTypeInstance:
|
||||
return analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE
|
||||
case domain.CountParentTypeOrganization:
|
||||
return analytics.CountParentType_COUNT_PARENT_TYPE_ORGANIZATION
|
||||
default:
|
||||
return analytics.CountParentType_COUNT_PARENT_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
type TelemetryError struct {
|
||||
StatusCode int
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (e *TelemetryError) Error() string {
|
||||
return fmt.Sprintf("telemetry error %d: %s", e.StatusCode, e.Body)
|
||||
}
|
18
internal/serviceping/config.go
Normal file
18
internal/serviceping/config.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package serviceping
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Endpoint string
|
||||
Interval string
|
||||
MaxAttempts uint8
|
||||
Telemetry TelemetryConfig
|
||||
}
|
||||
|
||||
type TelemetryConfig struct {
|
||||
ResourceCount ResourceCount
|
||||
}
|
||||
|
||||
type ResourceCount struct {
|
||||
Enabled bool
|
||||
BulkSize int
|
||||
}
|
5
internal/serviceping/mock/mock_gen.go
Normal file
5
internal/serviceping/mock/mock_gen.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package mock
|
||||
|
||||
//go:generate mockgen -package mock -destination queue.mock.go github.com/zitadel/zitadel/internal/serviceping Queue
|
||||
//go:generate mockgen -package mock -destination queries.mock.go github.com/zitadel/zitadel/internal/serviceping Queries
|
||||
//go:generate mockgen -package mock -destination telemetry.mock.go github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta TelemetryServiceClient
|
72
internal/serviceping/mock/queries.mock.go
Normal file
72
internal/serviceping/mock/queries.mock.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/serviceping (interfaces: Queries)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination queries.mock.go github.com/zitadel/zitadel/internal/serviceping Queries
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
query "github.com/zitadel/zitadel/internal/query"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockQueries is a mock of Queries interface.
|
||||
type MockQueries struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQueriesMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockQueriesMockRecorder is the mock recorder for MockQueries.
|
||||
type MockQueriesMockRecorder struct {
|
||||
mock *MockQueries
|
||||
}
|
||||
|
||||
// NewMockQueries creates a new mock instance.
|
||||
func NewMockQueries(ctrl *gomock.Controller) *MockQueries {
|
||||
mock := &MockQueries{ctrl: ctrl}
|
||||
mock.recorder = &MockQueriesMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQueries) EXPECT() *MockQueriesMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ListResourceCounts mocks base method.
|
||||
func (m *MockQueries) ListResourceCounts(ctx context.Context, lastID, size int) ([]query.ResourceCount, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListResourceCounts", ctx, lastID, size)
|
||||
ret0, _ := ret[0].([]query.ResourceCount)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListResourceCounts indicates an expected call of ListResourceCounts.
|
||||
func (mr *MockQueriesMockRecorder) ListResourceCounts(ctx, lastID, size any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourceCounts", reflect.TypeOf((*MockQueries)(nil).ListResourceCounts), ctx, lastID, size)
|
||||
}
|
||||
|
||||
// SearchInstances mocks base method.
|
||||
func (m *MockQueries) SearchInstances(ctx context.Context, queries *query.InstanceSearchQueries) (*query.Instances, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SearchInstances", ctx, queries)
|
||||
ret0, _ := ret[0].(*query.Instances)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SearchInstances indicates an expected call of SearchInstances.
|
||||
func (mr *MockQueriesMockRecorder) SearchInstances(ctx, queries any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstances", reflect.TypeOf((*MockQueries)(nil).SearchInstances), ctx, queries)
|
||||
}
|
62
internal/serviceping/mock/queue.mock.go
Normal file
62
internal/serviceping/mock/queue.mock.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/serviceping (interfaces: Queue)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination queue.mock.go github.com/zitadel/zitadel/internal/serviceping Queue
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
river "github.com/riverqueue/river"
|
||||
queue "github.com/zitadel/zitadel/internal/queue"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockQueue is a mock of Queue interface.
|
||||
type MockQueue struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQueueMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockQueueMockRecorder is the mock recorder for MockQueue.
|
||||
type MockQueueMockRecorder struct {
|
||||
mock *MockQueue
|
||||
}
|
||||
|
||||
// NewMockQueue creates a new mock instance.
|
||||
func NewMockQueue(ctrl *gomock.Controller) *MockQueue {
|
||||
mock := &MockQueue{ctrl: ctrl}
|
||||
mock.recorder = &MockQueueMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQueue) EXPECT() *MockQueueMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockQueue) Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, args}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Insert", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockQueueMockRecorder) Insert(ctx, args any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, args}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...)
|
||||
}
|
83
internal/serviceping/mock/telemetry.mock.go
Normal file
83
internal/serviceping/mock/telemetry.mock.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta (interfaces: TelemetryServiceClient)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination telemetry.mock.go github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta TelemetryServiceClient
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// MockTelemetryServiceClient is a mock of TelemetryServiceClient interface.
|
||||
type MockTelemetryServiceClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTelemetryServiceClientMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockTelemetryServiceClientMockRecorder is the mock recorder for MockTelemetryServiceClient.
|
||||
type MockTelemetryServiceClientMockRecorder struct {
|
||||
mock *MockTelemetryServiceClient
|
||||
}
|
||||
|
||||
// NewMockTelemetryServiceClient creates a new mock instance.
|
||||
func NewMockTelemetryServiceClient(ctrl *gomock.Controller) *MockTelemetryServiceClient {
|
||||
mock := &MockTelemetryServiceClient{ctrl: ctrl}
|
||||
mock.recorder = &MockTelemetryServiceClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTelemetryServiceClient) EXPECT() *MockTelemetryServiceClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ReportBaseInformation mocks base method.
|
||||
func (m *MockTelemetryServiceClient) ReportBaseInformation(ctx context.Context, in *analytics.ReportBaseInformationRequest, opts ...grpc.CallOption) (*analytics.ReportBaseInformationResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, in}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "ReportBaseInformation", varargs...)
|
||||
ret0, _ := ret[0].(*analytics.ReportBaseInformationResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReportBaseInformation indicates an expected call of ReportBaseInformation.
|
||||
func (mr *MockTelemetryServiceClientMockRecorder) ReportBaseInformation(ctx, in any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, in}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBaseInformation", reflect.TypeOf((*MockTelemetryServiceClient)(nil).ReportBaseInformation), varargs...)
|
||||
}
|
||||
|
||||
// ReportResourceCounts mocks base method.
|
||||
func (m *MockTelemetryServiceClient) ReportResourceCounts(ctx context.Context, in *analytics.ReportResourceCountsRequest, opts ...grpc.CallOption) (*analytics.ReportResourceCountsResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, in}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "ReportResourceCounts", varargs...)
|
||||
ret0, _ := ret[0].(*analytics.ReportResourceCountsResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReportResourceCounts indicates an expected call of ReportResourceCounts.
|
||||
func (mr *MockTelemetryServiceClientMockRecorder) ReportResourceCounts(ctx, in any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, in}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportResourceCounts", reflect.TypeOf((*MockTelemetryServiceClient)(nil).ReportResourceCounts), varargs...)
|
||||
}
|
17
internal/serviceping/report.go
Normal file
17
internal/serviceping/report.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package serviceping
|
||||
|
||||
type ReportType uint
|
||||
|
||||
const (
|
||||
ReportTypeBaseInformation ReportType = iota
|
||||
ReportTypeResourceCounts
|
||||
)
|
||||
|
||||
type ServicePingReport struct {
|
||||
ReportID string
|
||||
ReportType ReportType
|
||||
}
|
||||
|
||||
func (r *ServicePingReport) Kind() string {
|
||||
return "service_ping_report"
|
||||
}
|
252
internal/serviceping/worker.go
Normal file
252
internal/serviceping/worker.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package serviceping
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/cmd/build"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/queue"
|
||||
"github.com/zitadel/zitadel/internal/v2/system"
|
||||
analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta"
|
||||
)
|
||||
|
||||
const (
|
||||
QueueName = "service_ping_report"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidReportType = errors.New("invalid report type")
|
||||
|
||||
_ river.Worker[*ServicePingReport] = (*Worker)(nil)
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
river.WorkerDefaults[*ServicePingReport]
|
||||
|
||||
reportClient analytics.TelemetryServiceClient
|
||||
db Queries
|
||||
queue Queue
|
||||
|
||||
config *Config
|
||||
systemID string
|
||||
version string
|
||||
}
|
||||
|
||||
type Queries interface {
|
||||
SearchInstances(ctx context.Context, queries *query.InstanceSearchQueries) (*query.Instances, error)
|
||||
ListResourceCounts(ctx context.Context, lastID int, size int) ([]query.ResourceCount, error)
|
||||
}
|
||||
|
||||
type Queue interface {
|
||||
Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error
|
||||
}
|
||||
|
||||
// Register implements the [queue.Worker] interface.
|
||||
func (w *Worker) Register(workers *river.Workers, queues map[string]river.QueueConfig) {
|
||||
river.AddWorker[*ServicePingReport](workers, w)
|
||||
queues[QueueName] = river.QueueConfig{
|
||||
MaxWorkers: 1, // for now, we only use a single worker to prevent too much side effects on other queues
|
||||
}
|
||||
}
|
||||
|
||||
// Work implements the [river.Worker] interface.
|
||||
func (w *Worker) Work(ctx context.Context, job *river.Job[*ServicePingReport]) (err error) {
|
||||
defer func() {
|
||||
err = w.handleClientError(err)
|
||||
}()
|
||||
switch job.Args.ReportType {
|
||||
case ReportTypeBaseInformation:
|
||||
reportID, err := w.reportBaseInformation(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.createReportJobs(ctx, reportID)
|
||||
case ReportTypeResourceCounts:
|
||||
return w.reportResourceCounts(ctx, job.Args.ReportID)
|
||||
default:
|
||||
logging.WithFields("reportType", job.Args.ReportType, "reportID", job.Args.ReportID).
|
||||
Error("unknown job type")
|
||||
return river.JobCancel(ErrInvalidReportType)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) reportBaseInformation(ctx context.Context) (string, error) {
|
||||
instances, err := w.db.SearchInstances(ctx, &query.InstanceSearchQueries{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
instanceInformation := instanceInformationToPb(instances)
|
||||
resp, err := w.reportClient.ReportBaseInformation(ctx, &analytics.ReportBaseInformationRequest{
|
||||
SystemId: w.systemID,
|
||||
Version: w.version,
|
||||
Instances: instanceInformation,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetReportId(), nil
|
||||
}
|
||||
|
||||
func (w *Worker) reportResourceCounts(ctx context.Context, reportID string) error {
|
||||
lastID := 0
|
||||
// iterate over the resource counts until there are no more counts to report
|
||||
// or the context gets cancelled
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
counts, err := w.db.ListResourceCounts(ctx, lastID, w.config.Telemetry.ResourceCount.BulkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// if there are no counts, we can stop the loop
|
||||
if len(counts) == 0 {
|
||||
return nil
|
||||
}
|
||||
request := &analytics.ReportResourceCountsRequest{
|
||||
SystemId: w.systemID,
|
||||
ResourceCounts: resourceCountsToPb(counts),
|
||||
}
|
||||
if reportID != "" {
|
||||
request.ReportId = gu.Ptr(reportID)
|
||||
}
|
||||
resp, err := w.reportClient.ReportResourceCounts(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// in case the resource counts returned by the database are less than the bulk size,
|
||||
// we can assume that we have reached the end of the resource counts and can stop the loop
|
||||
if len(counts) < w.config.Telemetry.ResourceCount.BulkSize {
|
||||
return nil
|
||||
}
|
||||
// update the lastID for the next iteration
|
||||
lastID = counts[len(counts)-1].ID
|
||||
// In case we get a report ID back from the server (it could be the first call of the report),
|
||||
// we update it to use it for the next batch.
|
||||
if resp.GetReportId() != "" && resp.GetReportId() != reportID {
|
||||
reportID = resp.GetReportId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) handleClientError(err error) error {
|
||||
telemetryError := new(TelemetryError)
|
||||
if !errors.As(err, &telemetryError) {
|
||||
// If the error is not a TelemetryError, we can assume that it is a transient error
|
||||
// and can be retried by the queue.
|
||||
return err
|
||||
}
|
||||
switch telemetryError.StatusCode {
|
||||
case http.StatusBadRequest,
|
||||
http.StatusNotFound,
|
||||
http.StatusNotImplemented,
|
||||
http.StatusConflict,
|
||||
http.StatusPreconditionFailed:
|
||||
// In case of these errors, we can assume that a retry does not make sense,
|
||||
// so we can cancel the job.
|
||||
return river.JobCancel(err)
|
||||
default:
|
||||
// As of now we assume that all other errors are transient and can be retried.
|
||||
// So we just return the error, which will be handled by the queue as a failed attempt.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) createReportJobs(ctx context.Context, reportID string) error {
|
||||
errs := make([]error, 0)
|
||||
if w.config.Telemetry.ResourceCount.Enabled {
|
||||
err := w.addReportJob(ctx, reportID, ReportTypeResourceCounts)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (w *Worker) addReportJob(ctx context.Context, reportID string, reportType ReportType) error {
|
||||
job := &ServicePingReport{
|
||||
ReportID: reportID,
|
||||
ReportType: reportType,
|
||||
}
|
||||
return w.queue.Insert(ctx, job,
|
||||
queue.WithQueueName(QueueName),
|
||||
queue.WithMaxAttempts(w.config.MaxAttempts),
|
||||
)
|
||||
}
|
||||
|
||||
type systemIDReducer struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (s *systemIDReducer) Reduce() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemIDReducer) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
if idEvent, ok := event.(*system.IDGeneratedEvent); ok {
|
||||
s.id = idEvent.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *systemIDReducer) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(system.AggregateType).
|
||||
EventTypes(system.IDGeneratedType).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func Register(
|
||||
ctx context.Context,
|
||||
q *queue.Queue,
|
||||
queries *query.Queries,
|
||||
eventstoreClient *eventstore.Eventstore,
|
||||
config *Config,
|
||||
) error {
|
||||
if !config.Enabled {
|
||||
return nil
|
||||
}
|
||||
systemID := new(systemIDReducer)
|
||||
err := eventstoreClient.FilterToQueryReducer(ctx, systemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.AddWorkers(&Worker{
|
||||
reportClient: NewClient(config),
|
||||
db: queries,
|
||||
queue: q,
|
||||
config: config,
|
||||
systemID: systemID.id,
|
||||
version: build.Version(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func Start(config *Config, q *queue.Queue) error {
|
||||
if !config.Enabled {
|
||||
return nil
|
||||
}
|
||||
schedule, err := cron.ParseStandard(config.Interval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.AddPeriodicJob(
|
||||
schedule,
|
||||
&ServicePingReport{},
|
||||
queue.WithQueueName(QueueName),
|
||||
queue.WithMaxAttempts(config.MaxAttempts),
|
||||
)
|
||||
return nil
|
||||
}
|
1052
internal/serviceping/worker_test.go
Normal file
1052
internal/serviceping/worker_test.go
Normal file
File diff suppressed because it is too large
Load Diff
44
internal/v2/system/event.go
Normal file
44
internal/v2/system/event.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, IDGeneratedType, eventstore.GenericEventMapper[IDGeneratedEvent])
|
||||
}
|
||||
|
||||
const IDGeneratedType = AggregateType + ".id.generated"
|
||||
|
||||
type IDGeneratedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (e *IDGeneratedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *b
|
||||
}
|
||||
|
||||
func (e *IDGeneratedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *IDGeneratedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIDGeneratedEvent(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) *IDGeneratedEvent {
|
||||
return &IDGeneratedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
eventstore.NewAggregate(ctx, AggregateOwner, AggregateType, "v1"),
|
||||
IDGeneratedType),
|
||||
ID: id,
|
||||
}
|
||||
}
|
@@ -1,16 +1,26 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func WebAuthNsToCredentials(webAuthNs []*domain.WebAuthNToken, rpID string) []webauthn.Credential {
|
||||
func WebAuthNsToCredentials(ctx context.Context, webAuthNs []*domain.WebAuthNToken, rpID string) []webauthn.Credential {
|
||||
creds := make([]webauthn.Credential, 0)
|
||||
for _, webAuthN := range webAuthNs {
|
||||
if webAuthN.State == domain.MFAStateReady && webAuthN.RPID == rpID {
|
||||
// only add credentials that are ready and
|
||||
// either match the rpID or
|
||||
// if they were added through Console / old login UI, there is no stored rpID set;
|
||||
// then we check if the requested rpID matches the instance domain
|
||||
if webAuthN.State == domain.MFAStateReady &&
|
||||
(webAuthN.RPID == rpID ||
|
||||
(webAuthN.RPID == "" && rpID == strings.Split(http.DomainContext(ctx).InstanceHost, ":")[0])) {
|
||||
creds = append(creds, webauthn.Credential{
|
||||
ID: webAuthN.KeyID,
|
||||
PublicKey: webAuthN.PublicKey,
|
||||
|
153
internal/webauthn/converter_test.go
Normal file
153
internal/webauthn/converter_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func TestWebAuthNsToCredentials(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
webAuthNs []*domain.WebAuthNToken
|
||||
rpID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []webauthn.Credential
|
||||
}{
|
||||
{
|
||||
name: "unready credential",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
webAuthNs: []*domain.WebAuthNToken{
|
||||
{
|
||||
KeyID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
State: domain.MFAStateNotReady,
|
||||
},
|
||||
},
|
||||
rpID: "example.com",
|
||||
},
|
||||
want: []webauthn.Credential{},
|
||||
},
|
||||
{
|
||||
name: "not matching rpID",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
webAuthNs: []*domain.WebAuthNToken{
|
||||
{
|
||||
KeyID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
State: domain.MFAStateReady,
|
||||
RPID: "other.com",
|
||||
},
|
||||
},
|
||||
rpID: "example.com",
|
||||
},
|
||||
want: []webauthn.Credential{},
|
||||
},
|
||||
{
|
||||
name: "matching rpID",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
webAuthNs: []*domain.WebAuthNToken{
|
||||
{
|
||||
KeyID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
State: domain.MFAStateReady,
|
||||
RPID: "example.com",
|
||||
},
|
||||
},
|
||||
rpID: "example.com",
|
||||
},
|
||||
want: []webauthn.Credential{
|
||||
{
|
||||
ID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
Authenticator: webauthn.Authenticator{
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no rpID, different host",
|
||||
args: args{
|
||||
ctx: http.WithDomainContext(context.Background(), &http.DomainCtx{
|
||||
InstanceHost: "other.com:443",
|
||||
PublicHost: "other.com:443",
|
||||
Protocol: "https",
|
||||
}),
|
||||
webAuthNs: []*domain.WebAuthNToken{
|
||||
{
|
||||
KeyID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
State: domain.MFAStateReady,
|
||||
RPID: "",
|
||||
},
|
||||
},
|
||||
rpID: "example.com",
|
||||
},
|
||||
want: []webauthn.Credential{},
|
||||
},
|
||||
{
|
||||
name: "no rpID, same host",
|
||||
args: args{
|
||||
ctx: http.WithDomainContext(context.Background(), &http.DomainCtx{
|
||||
InstanceHost: "example.com:443",
|
||||
PublicHost: "example.com:443",
|
||||
Protocol: "https",
|
||||
}),
|
||||
webAuthNs: []*domain.WebAuthNToken{
|
||||
{
|
||||
KeyID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
State: domain.MFAStateReady,
|
||||
RPID: "",
|
||||
},
|
||||
},
|
||||
rpID: "example.com",
|
||||
},
|
||||
want: []webauthn.Credential{
|
||||
{
|
||||
ID: []byte("key1"),
|
||||
PublicKey: []byte("publicKey1"),
|
||||
AttestationType: "attestation1",
|
||||
Authenticator: webauthn.Authenticator{
|
||||
AAGUID: []byte("aaguid1"),
|
||||
SignCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, WebAuthNsToCredentials(tt.args.ctx, tt.args.webAuthNs, tt.args.rpID), "WebAuthNsToCredentials(%v, %v, %v)", tt.args.ctx, tt.args.webAuthNs, tt.args.rpID)
|
||||
})
|
||||
}
|
||||
}
|
@@ -57,7 +57,7 @@ func (w *Config) BeginRegistration(ctx context.Context, user *domain.Human, acco
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := WebAuthNsToCredentials(webAuthNs, rpID)
|
||||
creds := WebAuthNsToCredentials(ctx, webAuthNs, rpID)
|
||||
existing := make([]protocol.CredentialDescriptor, len(creds))
|
||||
for i, cred := range creds {
|
||||
existing[i] = protocol.CredentialDescriptor{
|
||||
@@ -136,7 +136,7 @@ func (w *Config) BeginLogin(ctx context.Context, user *domain.Human, userVerific
|
||||
}
|
||||
assertion, sessionData, err := webAuthNServer.BeginLogin(&webUser{
|
||||
Human: user,
|
||||
credentials: WebAuthNsToCredentials(webAuthNs, rpID),
|
||||
credentials: WebAuthNsToCredentials(ctx, webAuthNs, rpID),
|
||||
}, webauthn.WithUserVerification(UserVerificationFromDomain(userVerification)))
|
||||
if err != nil {
|
||||
logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn login could not be started")
|
||||
@@ -163,7 +163,7 @@ func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *
|
||||
}
|
||||
webUser := &webUser{
|
||||
Human: user,
|
||||
credentials: WebAuthNsToCredentials(webAuthNs, webAuthN.RPID),
|
||||
credentials: WebAuthNsToCredentials(ctx, webAuthNs, webAuthN.RPID),
|
||||
}
|
||||
webAuthNServer, err := w.serverFromContext(ctx, webAuthN.RPID, assertionData.Response.CollectedClientData.Origin)
|
||||
if err != nil {
|
||||
|
8
login/.changeset/README.md
Normal file
8
login/.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
11
login/.changeset/config.json
Normal file
11
login/.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@zitadel/login"]
|
||||
}
|
10
login/.eslintrc.cjs
Normal file
10
login/.eslintrc.cjs
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
// This tells ESLint to load the config from the package `@zitadel/eslint-config`
|
||||
extends: ["@zitadel/eslint-config"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["apps/*/"],
|
||||
},
|
||||
},
|
||||
};
|
63
login/.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
63
login/.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
@@ -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.
|
4
login/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
login/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💬 ZITADEL Community Chat
|
||||
url: https://zitadel.com/chat
|
30
login/.github/ISSUE_TEMPLATE/docs.yaml
vendored
Normal file
30
login/.github/ISSUE_TEMPLATE/docs.yaml
vendored
Normal file
@@ -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.
|
54
login/.github/ISSUE_TEMPLATE/improvement.yaml
vendored
Normal file
54
login/.github/ISSUE_TEMPLATE/improvement.yaml
vendored
Normal file
@@ -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.
|
54
login/.github/ISSUE_TEMPLATE/proposal.yaml
vendored
Normal file
54
login/.github/ISSUE_TEMPLATE/proposal.yaml
vendored
Normal file
@@ -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.
|
BIN
login/.github/custom-i18n.png
vendored
Normal file
BIN
login/.github/custom-i18n.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
21
login/.github/dependabot.yml
vendored
Normal file
21
login/.github/dependabot.yml
vendored
Normal file
@@ -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" ]
|
13
login/.github/pull_request_template.md
vendored
Normal file
13
login/.github/pull_request_template.md
vendored
Normal file
@@ -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
|
35
login/.github/workflows/close_pr.yml
vendored
Normal file
35
login/.github/workflows/close_pr.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Auto-close PRs and guide to correct repo
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-close:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_id == '622995060'
|
||||
steps:
|
||||
- name: Comment and close PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const message = `
|
||||
👋 **Thanks for your contribution!**
|
||||
|
||||
This repository \`${{ github.repository }}\` is a read-only mirror of our internal development in [\`zitadel/zitadel\`](https://github.com/zitadel/zitadel).
|
||||
Therefore, we close this pull request automatically, but submitting your changes to the main repository is easy:
|
||||
1. Fork and clone zitadel/zitadel
|
||||
2. Create a new branch for your changes
|
||||
3. Pull your changes into the new fork by running `make login_pull LOGIN_REMOTE_URL=<your-typescript-fork-org>/typescript LOGIN_REMOTE_BRANCH=<your-typescript-fork-branch>`.
|
||||
4. Push your changes and open a pull request to zitadel/zitadel
|
||||
`.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"
|
||||
});
|
41
login/.github/workflows/issues.yml
vendored
Normal file
41
login/.github/workflows/issues.yml
vendored
Normal file
@@ -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
|
32
login/.github/workflows/release.yml
vendored
Normal file
32
login/.github/workflows/release.yml
vendored
Normal file
@@ -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 }}
|
67
login/.github/workflows/test.yml
vendored
Normal file
67
login/.github/workflows/test.yml
vendored
Normal file
@@ -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()
|
18
login/.gitignore
vendored
Normal file
18
login/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.turbo
|
||||
*.log
|
||||
.next
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
server/dist
|
||||
public/dist
|
||||
.vscode
|
||||
.idea
|
||||
.vercel
|
||||
.env*.local
|
||||
/blob-report/
|
||||
/out
|
||||
/docker
|
1
login/.npmrc
Normal file
1
login/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
auto-install-peers = true
|
1
login/.nvmrc
Normal file
1
login/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
lts/iron
|
9
login/.prettierignore
Normal file
9
login/.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.next/
|
||||
.changeset/
|
||||
.github/
|
||||
dist/
|
||||
standalone/
|
||||
packages/zitadel-proto/google
|
||||
packages/zitadel-proto/protoc-gen-openapiv2
|
||||
packages/zitadel-proto/validate
|
||||
packages/zitadel-proto/zitadel
|
6
login/.prettierrc
Normal file
6
login/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 125,
|
||||
"trailingComma": "all",
|
||||
"plugins": ["prettier-plugin-organize-imports"],
|
||||
"filepath": ""
|
||||
}
|
128
login/CODE_OF_CONDUCT.md
Normal file
128
login/CODE_OF_CONDUCT.md
Normal file
@@ -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.
|
206
login/CONTRIBUTING.md
Normal file
206
login/CONTRIBUTING.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
### <a name="latest"></a>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.
|
||||
|
||||
### <a name="local"></a>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
|
||||
```
|
21
login/LICENSE
Normal file
21
login/LICENSE
Normal file
@@ -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.
|
137
login/Makefile
Normal file
137
login/Makefile
Normal file
@@ -0,0 +1,137 @@
|
||||
XDG_CACHE_HOME ?= $(HOME)/.cache
|
||||
export CACHE_DIR ?= $(XDG_CACHE_HOME)/zitadel-make
|
||||
|
||||
LOGIN_DIR ?= ./
|
||||
LOGIN_BAKE_CLI ?= docker buildx bake
|
||||
LOGIN_BAKE_CLI_WITH_ARGS := $(LOGIN_BAKE_CLI) --file $(LOGIN_DIR)docker-bake.hcl --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml
|
||||
LOGIN_BAKE_CLI_ADDITIONAL_ARGS ?=
|
||||
LOGIN_BAKE_CLI_WITH_ARGS += $(LOGIN_BAKE_CLI_ADDITIONAL_ARGS)
|
||||
|
||||
export COMPOSE_BAKE=true
|
||||
export UID := $(id -u)
|
||||
export GID := $(id -g)
|
||||
|
||||
export LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT := $(LOGIN_DIR)apps/login-test-acceptance
|
||||
|
||||
export DOCKER_METADATA_OUTPUT_VERSION ?= local
|
||||
export LOGIN_TAG ?= login:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_UNIT_TAG := login-test-unit:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_INTEGRATION_TAG := login-test-integration:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_TAG := login-test-acceptance:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_SETUP_TAG := login-test-acceptance-setup:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_SINK_TAG := login-test-acceptance-sink:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_OIDCRP_TAG := login-test-acceptance-oidcrp:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_OIDCOP_TAG := login-test-acceptance-oidcop:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG := login-test-acceptance-samlsp:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG := login-test-acceptance-samlidp:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export POSTGRES_TAG := postgres:17.0-alpine3.19
|
||||
export GOLANG_TAG := golang:1.24-alpine
|
||||
export ZITADEL_TAG ?= ghcr.io/zitadel/zitadel:latest
|
||||
export LOGIN_CORE_MOCK_TAG := login-core-mock:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
|
||||
login_help:
|
||||
@echo "Makefile for the login service"
|
||||
@echo "Available targets:"
|
||||
@echo " login_help - Show this help message."
|
||||
@echo " login_quality - Run all quality checks (login_lint, login_test_unit, login_test_integration, login_test_acceptance)."
|
||||
@echo " login_standalone_build - Build the docker image for production login containers."
|
||||
@echo " login_lint - Run linting and formatting checks. IGNORE_RUN_CACHE=true prevents skipping."
|
||||
@echo " login_test_unit - Run unit tests. Tests without any dependencies. IGNORE_RUN_CACHE=true prevents skipping."
|
||||
@echo " login-test_integration - Run integration tests. Tests a login production build against a mocked Zitadel core API. IGNORE_RUN_CACHE=true prevents skipping."
|
||||
@echo " login_test_acceptance - Run acceptance tests. Tests a login production build with a local Zitadel instance behind a reverse proxy. IGNORE_RUN_CACHE=true prevents skipping."
|
||||
@echo " typescript_generate - Generate TypeScript client code from Protobuf definitions."
|
||||
@echo " show_run_caches - Show all run caches with image ids and exit codes."
|
||||
@echo " clean_run_caches - Remove all run caches."
|
||||
|
||||
|
||||
login_lint:
|
||||
@echo "Running login linting and formatting checks"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) login-lint
|
||||
|
||||
login_test_unit:
|
||||
@echo "Running login unit tests"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) login-test-unit
|
||||
|
||||
login_test_integration_build:
|
||||
@echo "Building login integration test environment with the local core mock image"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) core-mock login-test-integration login-standalone --load
|
||||
|
||||
login_test_integration_dev: login_test_integration_cleanup
|
||||
@echo "Starting login integration test environment with the local core mock image"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) core-mock && docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --service-ports --rm core-mock
|
||||
|
||||
login_test_integration_run: login_test_integration_cleanup
|
||||
@echo "Running login integration tests"
|
||||
docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --rm integration
|
||||
|
||||
login_test_integration_cleanup:
|
||||
@echo "Cleaning up login integration test environment"
|
||||
docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml down --volumes
|
||||
|
||||
login_test_integration: login_test_integration_build
|
||||
$(LOGIN_DIR)scripts/run_or_skip.sh login_test_integration_run \
|
||||
"$(LOGIN_TAG) \
|
||||
$(LOGIN_CORE_MOCK_TAG) \
|
||||
$(LOGIN_TEST_INTEGRATION_TAG)"
|
||||
|
||||
login_test_acceptance_build_bake:
|
||||
@echo "Building login test acceptance images as defined in the docker-bake.hcl"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) login-test-acceptance login-standalone --load
|
||||
|
||||
login_test_acceptance_build_compose:
|
||||
@echo "Building login test acceptance images as defined in the docker-compose.yaml"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) --load setup sink
|
||||
|
||||
# login_test_acceptance_build is overwritten by the login_dev target in zitadel/zitadel/Makefile
|
||||
login_test_acceptance_build: login_test_acceptance_build_compose login_test_acceptance_build_bake
|
||||
|
||||
login_test_acceptance_run: login_test_acceptance_cleanup
|
||||
@echo "Running login test acceptance tests"
|
||||
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose-ci.yaml run --rm --service-ports acceptance
|
||||
|
||||
login_test_acceptance_cleanup:
|
||||
@echo "Cleaning up login test acceptance environment"
|
||||
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose-ci.yaml down --volumes
|
||||
|
||||
login_test_acceptance: login_test_acceptance_build
|
||||
$(LOGIN_DIR)scripts/run_or_skip.sh login_test_acceptance_run \
|
||||
"$(LOGIN_TAG) \
|
||||
$(ZITADEL_TAG) \
|
||||
$(POSTGRES_TAG) \
|
||||
$(GOLANG_TAG) \
|
||||
$(LOGIN_TEST_ACCEPTANCE_TAG) \
|
||||
$(LOGIN_TEST_ACCEPTANCE_SETUP_TAG) \
|
||||
$(LOGIN_TEST_ACCEPTANCE_SINK_TAG)"
|
||||
|
||||
login_test_acceptance_setup_env: login_test_acceptance_build_compose login_test_acceptance_cleanup
|
||||
@echo "Setting up the login test acceptance environment and writing the env.test.local file"
|
||||
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml run setup
|
||||
|
||||
login_test_acceptance_setup_dev:
|
||||
@echo "Starting the login test acceptance environment with the local zitadel image"
|
||||
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml up --no-recreate zitadel traefik sink
|
||||
|
||||
login_quality: login_lint login_test_unit login_test_integration
|
||||
@echo "Running login quality checks: lint, unit tests, integration tests"
|
||||
|
||||
login_standalone_build:
|
||||
@echo "Building the login standalone docker image with tag: $(LOGIN_TAG)"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) login-standalone --load
|
||||
|
||||
login_standalone_out:
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) login-standalone-out
|
||||
|
||||
typescript_generate:
|
||||
@echo "Generating TypeScript client and writing to local $(LOGIN_DIR)packages/zitadel-proto"
|
||||
$(LOGIN_BAKE_CLI_WITH_ARGS) login-typescript-proto-client-out
|
||||
|
||||
clean_run_caches:
|
||||
@echo "Removing cache directory: $(CACHE_DIR)"
|
||||
rm -rf "$(CACHE_DIR)"
|
||||
|
||||
show_run_caches:
|
||||
@echo "Showing run caches with docker image ids and exit codes in $(CACHE_DIR):"
|
||||
@find "$(CACHE_DIR)" -type f 2>/dev/null | while read file; do \
|
||||
echo "$$file: $$(cat $$file)"; \
|
||||
done
|
||||
|
264
login/README.md
Normal file
264
login/README.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# ZITADEL TypeScript with Turborepo
|
||||
|
||||
This repository contains all TypeScript and JavaScript packages and applications you need to create your own ZITADEL
|
||||
Login UI.
|
||||
|
||||
<img src="./apps/login/screenshots/collage.png" alt="collage of login screens" width="1600px" />
|
||||
|
||||
[](https://www.npmjs.com/package/@zitadel/proto)
|
||||
[](https://www.npmjs.com/package/@zitadel/client)
|
||||
|
||||
**⚠️ This repo and packages are in beta state and subject to change ⚠️**
|
||||
|
||||
The scope of functionality of this repo and packages is under active development.
|
||||
|
||||
The `@zitadel/client` package is using [@connectrpc/connect](https://github.com/connectrpc/connect-es#readme).
|
||||
|
||||
You can read the [contribution guide](/CONTRIBUTING.md) on how to contribute.
|
||||
Questions can be raised in our [Discord channel](https://discord.gg/erh5Brh7jE) or as
|
||||
a [GitHub issue](https://github.com/zitadel/typescript/issues).
|
||||
|
||||
## Developing Your Own ZITADEL Login UI
|
||||
|
||||
We think the easiest path of getting up and running, is the following:
|
||||
|
||||
1. Fork and clone this repository
|
||||
1. [Run the ZITADEL Cloud login UI locally](#run-login-ui)
|
||||
1. Make changes to the code and see the effects live on your local machine
|
||||
1. Study the rest of this README.md and get familiar and comfortable with how everything works.
|
||||
1. Decide on a way of how you want to build and run your login UI.
|
||||
You can reuse ZITADEL Clouds way.
|
||||
But if you need more freedom, you can also import the packages you need into your self built application.
|
||||
|
||||
## Included Apps And Packages
|
||||
|
||||
- `login`: The login UI used by ZITADEL Cloud, powered by Next.js
|
||||
- `@zitadel/client`: shared client utilities for node and browser environments
|
||||
- `@zitadel/proto`: Protocol Buffers (proto) definitions used by ZITADEL projects
|
||||
- `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo
|
||||
- `@zitadel/eslint-config`: ESLint preset
|
||||
|
||||
Each package and app is 100% [TypeScript](https://www.typescriptlang.org/).
|
||||
|
||||
### Login
|
||||
|
||||
The login is currently in a work in progress state.
|
||||
The goal is to implement a login UI, using the session API of ZITADEL, which also implements the OIDC Standard and is
|
||||
ready to use for everyone.
|
||||
|
||||
In the first phase we want to have a MVP login ready with the OIDC Standard and a basic feature set. In a second step
|
||||
the features will be extended.
|
||||
|
||||
This list should show the current implementation state, and also what is missing.
|
||||
You can already use the current state, and extend it with your needs.
|
||||
|
||||
#### Features list
|
||||
|
||||
- [x] Local User Registration (with Password)
|
||||
- [x] User Registration and Login with external Provider
|
||||
- [x] Google
|
||||
- [x] GitHub
|
||||
- [x] GitHub Enterprise
|
||||
- [x] GitLab
|
||||
- [x] GitLab Enterprise
|
||||
- [x] Azure
|
||||
- [x] Apple
|
||||
- [x] Generic OIDC
|
||||
- [x] Generic OAuth
|
||||
- [x] Generic JWT
|
||||
- [x] LDAP
|
||||
- [x] SAML SP
|
||||
- Multifactor Registration an Login
|
||||
- [x] Passkeys
|
||||
- [x] TOTP
|
||||
- [x] OTP: Email Code
|
||||
- [x] OTP: SMS Code
|
||||
- [x] Password Change/Reset
|
||||
- [x] Domain Discovery
|
||||
- [x] Branding
|
||||
- OIDC Standard
|
||||
|
||||
- [x] Authorization Code Flow with PKCE
|
||||
- [x] AuthRequest `hintUserId`
|
||||
- [x] AuthRequest `loginHint`
|
||||
- [x] AuthRequest `prompt`
|
||||
- [x] Login
|
||||
- [x] Select Account
|
||||
- [ ] Consent
|
||||
- [x] Create
|
||||
- Scopes
|
||||
- [x] `openid email profile address``
|
||||
- [x] `offline access`
|
||||
- [x] `urn:zitadel:iam:org:idp:id:{idp_id}`
|
||||
- [x] `urn:zitadel:iam:org:project:id:zitadel:aud`
|
||||
- [x] `urn:zitadel:iam:org:id:{orgid}`
|
||||
- [x] `urn:zitadel:iam:org:domain:primary:{domain}`
|
||||
- [ ] AuthRequest UI locales
|
||||
|
||||
#### Flow diagram
|
||||
|
||||
This diagram shows the available pages and flows.
|
||||
|
||||
> Note that back navigation or retries are not displayed.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> register
|
||||
A[Start] --> accounts
|
||||
A[Start] --> loginname
|
||||
loginname -- signInWithIDP --> idp-success
|
||||
loginname -- signInWithIDP --> idp-failure
|
||||
idp-success --> B[signedin]
|
||||
loginname --> password
|
||||
loginname -- hasPasskey --> passkey
|
||||
loginname -- allowRegister --> register
|
||||
passkey-add --passwordAllowed --> password
|
||||
passkey -- hasPassword --> password
|
||||
passkey --> B[signedin]
|
||||
password -- hasMFA --> mfa
|
||||
password -- allowPasskeys --> passkey-add
|
||||
password -- reset --> password-set
|
||||
email -- reset --> password-set
|
||||
password-set --> B[signedin]
|
||||
password-change --> B[signedin]
|
||||
password -- userstate=initial --> password-change
|
||||
|
||||
mfa --> otp
|
||||
otp --> B[signedin]
|
||||
mfa--> u2f
|
||||
u2f -->B[signedin]
|
||||
register -- password/passkey --> B[signedin]
|
||||
password --> B[signedin]
|
||||
password-- forceMFA -->mfaset
|
||||
mfaset --> u2fset
|
||||
mfaset --> otpset
|
||||
u2fset --> B[signedin]
|
||||
otpset --> B[signedin]
|
||||
accounts--> loginname
|
||||
password -- not verified yet -->verify
|
||||
register-- withpassword -->verify
|
||||
passkey-- notVerified --> verify
|
||||
verify --> B[signedin]
|
||||
```
|
||||
|
||||
You can find a more detailed documentation of the different pages [here](./apps/login/readme.md).
|
||||
|
||||
#### Custom translations
|
||||
|
||||
The new login uses the [SettingsApi](https://zitadel.com/docs/apis/resources/settings_service_v2/settings-service-get-hosted-login-translation) to load custom translations.
|
||||
Translations can be overriden at both the instance and organization levels.
|
||||
To find the keys more easily, you can inspect the HTML and search for a `data-i18n-key` attribute, or look at the defaults in `/apps/login/locales/[locale].ts`.
|
||||

|
||||
|
||||
## Tooling
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
||||
- [ESLint](https://eslint.org/) for code linting
|
||||
- [Prettier](https://prettier.io) for code formatting
|
||||
|
||||
## Useful Commands
|
||||
|
||||
- `make login-quality` - Check the quality of your code against a production build without installing any dependencies besides Docker
|
||||
- `pnpm generate` - Build proto stubs for the client package
|
||||
- `pnpm dev` - Develop all packages and the login app
|
||||
- `pnpm build` - Build all packages and the login app
|
||||
- `pnpm clean` - Clean up all `node_modules` and `dist` folders (runs each package's clean script)
|
||||
|
||||
Learn more about developing the login UI in the [contribution guide](/CONTRIBUTING.md).
|
||||
|
||||
## Versioning And Publishing Packages
|
||||
|
||||
Package publishing has been configured using [Changesets](https://github.com/changesets/changesets).
|
||||
Here is their [documentation](https://github.com/changesets/changesets#documentation) for more information about the
|
||||
workflow.
|
||||
|
||||
The [GitHub Action](https://github.com/changesets/action) needs an `NPM_TOKEN` and `GITHUB_TOKEN` in the repository
|
||||
settings. The [Changesets bot](https://github.com/apps/changeset-bot) should also be installed on the GitHub repository.
|
||||
|
||||
Read the [changesets documentation](https://github.com/changesets/changesets/blob/main/docs/automating-changesets.md)
|
||||
for more information about this automation
|
||||
|
||||
### Run Login UI
|
||||
|
||||
To run the application make sure to install the dependencies with
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
then generate the GRPC stubs with
|
||||
|
||||
```sh
|
||||
pnpm generate
|
||||
```
|
||||
|
||||
To run the application against a local ZITADEL instance, run the following command:
|
||||
|
||||
```sh
|
||||
pnpm run-zitadel
|
||||
```
|
||||
|
||||
This sets up ZITADEL using docker compose and writes the configuration to the file `apps/login/.env.local`.
|
||||
|
||||
<details>
|
||||
<summary>Alternatively, use another environment</summary>
|
||||
You can develop against any ZITADEL instance in which you have sufficient rights to execute the following steps.
|
||||
Just create or overwrite the file `apps/login/.env.local` yourself.
|
||||
Add your instances base URL to the file at the key `ZITADEL_API_URL`.
|
||||
Go to your instance and create a service user for the login application.
|
||||
The login application creates users on your primary organization and reads policy data.
|
||||
For the sake of simplicity, just make the service user an instance member with the role `IAM_OWNER`.
|
||||
Create a PAT and copy it to the file `apps/login/.env.local` using the key `ZITADEL_SERVICE_USER_TOKEN`.
|
||||
|
||||
The file should look similar to this:
|
||||
|
||||
```
|
||||
ZITADEL_API_URL=https://zitadel-tlx3du.us1.zitadel.cloud
|
||||
ZITADEL_SERVICE_USER_TOKEN=1S6w48thfWFI2klgfwkCnhXJLf9FQ457E-_3H74ePQxfO3Af0Tm4V5Xi-ji7urIl_xbn-Rk
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Start the login application in dev mode:
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open the login application with your favorite browser at `localhost:3000`.
|
||||
Change the source code and see the changes live in your browser.
|
||||
|
||||
Make sure the application still behaves as expected by running all tests
|
||||
|
||||
```sh
|
||||
pnpm test
|
||||
```
|
||||
|
||||
To satisfy your unique workflow requirements, check out the package.json in the root directory for more detailed scripts.
|
||||
|
||||
### Run Login UI Acceptance tests
|
||||
|
||||
To run the acceptance tests you need a running ZITADEL environment and a component which receives HTTP requests for the emails and sms's.
|
||||
This component should also be able to return the content of these notifications, as the codes and links are used in the login flows.
|
||||
There is a basic implementation in Golang available under [the sink package](./acceptance/sink).
|
||||
|
||||
To setup ZITADEL with the additional Sink container for handling the notifications:
|
||||
|
||||
```sh
|
||||
pnpm run-sink
|
||||
```
|
||||
|
||||
Then you can start the acceptance tests with:
|
||||
|
||||
```sh
|
||||
pnpm test:acceptance
|
||||
```
|
||||
|
||||
### Deploy to Vercel
|
||||
|
||||
To deploy your own version on Vercel, navigate to your instance and create a service user.
|
||||
Then create a personal access token (PAT), copy and set it as ZITADEL_SERVICE_USER_TOKEN, then navigate to your instance
|
||||
settings and make sure it gets IAM_OWNER permissions.
|
||||
Finally set your instance url as ZITADEL_API_URL. Make sure to set it without trailing slash.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_LOGIN_CLIENT%20membership%20on%20your%20instance%20and%20provide%20its%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login)
|
71
login/acceptance/docker-compose.yaml
Normal file
71
login/acceptance/docker-compose.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
services:
|
||||
zitadel:
|
||||
user: "${ZITADEL_DEV_UID}"
|
||||
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:02617cf17fdde849378c1a6b5254bbfb2745b164}"
|
||||
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./pat:/pat
|
||||
- ./zitadel.yaml:/zitadel.yaml
|
||||
depends_on:
|
||||
db:
|
||||
condition: "service_healthy"
|
||||
extra_hosts:
|
||||
- "localhost:host-gateway"
|
||||
|
||||
db:
|
||||
restart: "always"
|
||||
image: postgres:17.0-alpine3.19
|
||||
environment:
|
||||
- POSTGRES_USER=zitadel
|
||||
- PGUSER=zitadel
|
||||
- POSTGRES_DB=zitadel
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: "10s"
|
||||
timeout: "30s"
|
||||
retries: 5
|
||||
start_period: "20s"
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
wait_for_zitadel:
|
||||
image: curlimages/curl:8.00.1
|
||||
command: /bin/sh -c "until curl -s -o /dev/null -i -f http://zitadel:8080/debug/ready; do echo 'waiting' && sleep 1; done; echo 'ready' && sleep 5;" || false
|
||||
depends_on:
|
||||
- zitadel
|
||||
|
||||
setup:
|
||||
user: "${ZITADEL_DEV_UID}"
|
||||
container_name: setup
|
||||
image: acceptance-setup:latest
|
||||
environment:
|
||||
PAT_FILE: /pat/zitadel-admin-sa.pat
|
||||
ZITADEL_API_INTERNAL_URL: http://zitadel:8080
|
||||
WRITE_ENVIRONMENT_FILE: /apps/login/.env.local
|
||||
WRITE_TEST_ENVIRONMENT_FILE: /acceptance/tests/.env.local
|
||||
SINK_EMAIL_INTERNAL_URL: http://sink:3333/email
|
||||
SINK_SMS_INTERNAL_URL: http://sink:3333/sms
|
||||
SINK_NOTIFICATION_URL: http://localhost:3333/notification
|
||||
volumes:
|
||||
- "./pat:/pat"
|
||||
- "../apps/login:/apps/login"
|
||||
- "../acceptance/tests:/acceptance/tests"
|
||||
depends_on:
|
||||
wait_for_zitadel:
|
||||
condition: "service_completed_successfully"
|
||||
|
||||
sink:
|
||||
image: golang:1.24-alpine
|
||||
container_name: sink
|
||||
command: go run /sink/main.go -port '3333' -email '/email' -sms '/sms' -notification '/notification'
|
||||
ports:
|
||||
- 3333:3333
|
||||
volumes:
|
||||
- "./sink:/sink"
|
||||
depends_on:
|
||||
setup:
|
||||
condition: "service_completed_successfully"
|
1
login/apps/login-test-acceptance/.gitignore
vendored
Normal file
1
login/apps/login-test-acceptance/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
go-command
|
59
login/apps/login-test-acceptance/docker-compose-ci.yaml
Normal file
59
login/apps/login-test-acceptance/docker-compose-ci.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
services:
|
||||
|
||||
zitadel:
|
||||
environment:
|
||||
ZITADEL_EXTERNALDOMAIN: traefik
|
||||
|
||||
traefik:
|
||||
labels: !reset []
|
||||
|
||||
setup:
|
||||
environment:
|
||||
ZITADEL_API_DOMAIN: traefik
|
||||
ZITADEL_API_URL: https://traefik
|
||||
LOGIN_BASE_URL: https://traefik/ui/v2/login/
|
||||
SINK_NOTIFICATION_URL: http://sink:3333/notification
|
||||
ZITADEL_ADMIN_USER: zitadel-admin@zitadel.traefik
|
||||
|
||||
login:
|
||||
image: "${LOGIN_TAG:-login:local}"
|
||||
container_name: acceptance-login
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
acceptance:
|
||||
image: "${LOGIN_TEST_ACCEPTANCE_TAG:-login-test-acceptance:local}"
|
||||
container_name: acceptance
|
||||
environment:
|
||||
- CI
|
||||
- LOGIN_BASE_URL=https://traefik/ui/v2/login/
|
||||
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
volumes:
|
||||
- ../login/.env.test.local:/build/apps/login/.env.test.local
|
||||
- ./test-results:/build/apps/login-test-acceptance/test-results
|
||||
- ./playwright-report:/build/apps/login-test-acceptance/playwright-report
|
||||
ports:
|
||||
- 9323:9323
|
||||
ipc: "host"
|
||||
init: true
|
||||
depends_on:
|
||||
login:
|
||||
condition: "service_healthy"
|
||||
sink:
|
||||
condition: service_healthy
|
||||
# oidcrp:
|
||||
# condition: service_healthy
|
||||
# oidcop:
|
||||
# condition: service_healthy
|
||||
# samlsp:
|
||||
# condition: service_healthy
|
||||
# samlidp:
|
||||
# condition: service_healthy
|
237
login/apps/login-test-acceptance/docker-compose.yaml
Normal file
237
login/apps/login-test-acceptance/docker-compose.yaml
Normal file
@@ -0,0 +1,237 @@
|
||||
services:
|
||||
|
||||
zitadel:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: "${ZITADEL_TAG:-ghcr.io/zitadel/zitadel:latest}"
|
||||
container_name: acceptance-zitadel
|
||||
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml'
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)"
|
||||
# - "traefik.http.middlewares.zitadel.headers.customrequestheaders.Host=localhost"
|
||||
# - "traefik.http.routers.zitadel.middlewares=zitadel@docker"
|
||||
- "traefik.http.services.zitadel-service.loadbalancer.server.scheme=h2c"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./pat:/pat
|
||||
- ./zitadel.yaml:/zitadel.yaml
|
||||
depends_on:
|
||||
db:
|
||||
condition: "service_healthy"
|
||||
|
||||
db:
|
||||
restart: "always"
|
||||
image: ${LOGIN_TEST_ACCEPTANCE_POSTGES_TAG:-postgres:17.0-alpine3.19}
|
||||
container_name: acceptance-db
|
||||
environment:
|
||||
- POSTGRES_USER=zitadel
|
||||
- PGUSER=zitadel
|
||||
- POSTGRES_DB=zitadel
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: "10s"
|
||||
timeout: "30s"
|
||||
retries: 5
|
||||
start_period: "20s"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
wait-for-zitadel:
|
||||
image: curlimages/curl:8.00.1
|
||||
container_name: acceptance-wait-for-zitadel
|
||||
command: /bin/sh -c "until curl -s -o /dev/null -i -f http://zitadel:8080/debug/ready; do echo 'waiting' && sleep 1; done; echo 'ready' && sleep 5;" || false
|
||||
depends_on:
|
||||
- zitadel
|
||||
|
||||
traefik:
|
||||
image: "traefik:v3.4"
|
||||
container_name: "acceptance-traefik"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
|
||||
- "traefik.http.services.login-service.loadbalancer.server.url=http://host.docker.internal:3000"
|
||||
command:
|
||||
# - "--log.level=DEBUG"
|
||||
- "--ping"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.websecure.http.tls=true"
|
||||
- "--entryPoints.websecure.address=:443"
|
||||
healthcheck:
|
||||
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
||||
interval: "10s"
|
||||
timeout: "30s"
|
||||
retries: 5
|
||||
start_period: "20s"
|
||||
ports:
|
||||
- "443:443"
|
||||
- "8090:8080"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
|
||||
setup:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: ${LOGIN_TEST_ACCEPTANCE_SETUP_TAG:-login-test-acceptance-setup:local}
|
||||
container_name: acceptance-setup
|
||||
restart: no
|
||||
build:
|
||||
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/setup"
|
||||
dockerfile: ../go-command.Dockerfile
|
||||
entrypoint: "./setup.sh"
|
||||
environment:
|
||||
PAT_FILE: /pat/zitadel-admin-sa.pat
|
||||
ZITADEL_API_INTERNAL_URL: http://zitadel: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://localhost:3333/notification
|
||||
LOGIN_BASE_URL: https://127.0.0.1.sslip.io/ui/v2/login/
|
||||
ZITADEL_API_URL: https://127.0.0.1.sslip.io
|
||||
ZITADEL_API_DOMAIN: 127.0.0.1.sslip.io
|
||||
ZITADEL_ADMIN_USER: zitadel-admin@zitadel.127.0.0.1.sslip.io
|
||||
volumes:
|
||||
- ./pat:/pat # Read the PAT file from zitadels setup
|
||||
- ../login:/login-env # Write the environment variables file for the login
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: "service_healthy"
|
||||
wait-for-zitadel:
|
||||
condition: "service_completed_successfully"
|
||||
|
||||
sink:
|
||||
image: ${LOGIN_TEST_ACCEPTANCE_SINK_TAG:-login-test-acceptance-sink:local}
|
||||
container_name: acceptance-sink
|
||||
build:
|
||||
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/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:
|
||||
setup:
|
||||
condition: "service_completed_successfully"
|
||||
|
||||
oidcrp:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: ${LOGIN_TEST_ACCEPTANCE_OIDCRP_TAG:-login-test-acceptance-oidcrp:local}
|
||||
container_name: acceptance-oidcrp
|
||||
build:
|
||||
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/oidcrp"
|
||||
dockerfile: ../go-command.Dockerfile
|
||||
args:
|
||||
- LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
|
||||
environment:
|
||||
API_URL: 'http://traefik'
|
||||
API_DOMAIN: 'traefik'
|
||||
PAT_FILE: '/pat/zitadel-admin-sa.pat'
|
||||
LOGIN_URL: 'https://traefik/ui/v2/login'
|
||||
ISSUER: 'https://traefik'
|
||||
HOST: 'traefik'
|
||||
PORT: '8000'
|
||||
SCOPES: 'openid profile email'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "./pat:/pat"
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: "service_healthy"
|
||||
setup:
|
||||
condition: "service_completed_successfully"
|
||||
|
||||
oidcop:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: ${LOGIN_TEST_ACCEPTANCE_OIDCOP_TAG:-login-test-acceptance-oidcop:local}
|
||||
container_name: acceptance-oidcop
|
||||
build:
|
||||
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/idp/oidc"
|
||||
dockerfile: ../../go-command.Dockerfile
|
||||
args:
|
||||
- LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
|
||||
environment:
|
||||
API_URL: 'http://traefik'
|
||||
API_DOMAIN: 'traefik'
|
||||
PAT_FILE: '/pat/zitadel-admin-sa.pat'
|
||||
SCHEMA: 'https'
|
||||
HOST: 'traefik'
|
||||
PORT: "8004"
|
||||
ports:
|
||||
- 8004:8004
|
||||
volumes:
|
||||
- "./pat:/pat"
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: "service_healthy"
|
||||
setup:
|
||||
condition: "service_completed_successfully"
|
||||
|
||||
samlsp:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: "${LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG:-login-test-acceptance-samlsp:local}"
|
||||
container_name: acceptance-samlsp
|
||||
build:
|
||||
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/samlsp"
|
||||
dockerfile: ../go-command.Dockerfile
|
||||
args:
|
||||
- LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
|
||||
environment:
|
||||
API_URL: 'http://traefik'
|
||||
API_DOMAIN: 'traefik'
|
||||
PAT_FILE: '/pat/zitadel-admin-sa.pat'
|
||||
LOGIN_URL: 'https://traefik/ui/v2/login'
|
||||
IDP_URL: 'http://zitadel:8080/saml/v2/metadata'
|
||||
HOST: 'https://traefik'
|
||||
PORT: '8001'
|
||||
ports:
|
||||
- 8001:8001
|
||||
volumes:
|
||||
- "./pat:/pat"
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: "service_healthy"
|
||||
setup:
|
||||
condition: "service_completed_successfully"
|
||||
|
||||
samlidp:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: "${LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG:-login-test-acceptance-samlidp:local}"
|
||||
container_name: acceptance-samlidp
|
||||
build:
|
||||
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/idp/saml"
|
||||
dockerfile: ../../go-command.Dockerfile
|
||||
args:
|
||||
- LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine}
|
||||
environment:
|
||||
API_URL: 'http://traefik:8080'
|
||||
API_DOMAIN: 'traefik'
|
||||
PAT_FILE: '/pat/zitadel-admin-sa.pat'
|
||||
SCHEMA: 'https'
|
||||
HOST: 'traefik'
|
||||
PORT: "8003"
|
||||
ports:
|
||||
- 8003:8003
|
||||
volumes:
|
||||
- "./pat:/pat"
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: "service_healthy"
|
||||
setup:
|
||||
condition: "service_completed_successfully"
|
11
login/apps/login-test-acceptance/go-command.Dockerfile
Normal file
11
login/apps/login-test-acceptance/go-command.Dockerfile
Normal file
@@ -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" ]
|
28
login/apps/login-test-acceptance/idp/oidc/go.mod
Normal file
28
login/apps/login-test-acceptance/idp/oidc/go.mod
Normal file
@@ -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
|
||||
)
|
71
login/apps/login-test-acceptance/idp/oidc/go.sum
Normal file
71
login/apps/login-test-acceptance/idp/oidc/go.sum
Normal file
@@ -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=
|
186
login/apps/login-test-acceptance/idp/oidc/main.go
Normal file
186
login/apps/login-test-acceptance/idp/oidc/main.go
Normal file
@@ -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
|
||||
}
|
16
login/apps/login-test-acceptance/idp/saml/go.mod
Normal file
16
login/apps/login-test-acceptance/idp/saml/go.mod
Normal file
@@ -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
|
||||
)
|
49
login/apps/login-test-acceptance/idp/saml/go.sum
Normal file
49
login/apps/login-test-acceptance/idp/saml/go.sum
Normal file
@@ -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=
|
328
login/apps/login-test-acceptance/idp/saml/main.go
Normal file
328
login/apps/login-test-acceptance/idp/saml/main.go
Normal file
@@ -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 <EntityDescriptor> but have <EntitiesDescriptor>" {
|
||||
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
|
||||
}
|
26
login/apps/login-test-acceptance/oidcrp/go.mod
Normal file
26
login/apps/login-test-acceptance/oidcrp/go.mod
Normal file
@@ -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
|
||||
)
|
67
login/apps/login-test-acceptance/oidcrp/go.sum
Normal file
67
login/apps/login-test-acceptance/oidcrp/go.sum
Normal file
@@ -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=
|
322
login/apps/login-test-acceptance/oidcrp/main.go
Normal file
322
login/apps/login-test-acceptance/oidcrp/main.go
Normal file
@@ -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
|
||||
}
|
18
login/apps/login-test-acceptance/package.json
Normal file
18
login/apps/login-test-acceptance/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "login-test-acceptance",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test:acceptance": "dotenv -e ../login/.env.test.local pnpm exec playwright",
|
||||
"test:acceptance:setup": "cd ../.. && make login_test_acceptance_setup_env && NODE_ENV=test pnpm exec 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",
|
||||
"gaxios": "^7.1.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
2
login/apps/login-test-acceptance/pat/.gitignore
vendored
Normal file
2
login/apps/login-test-acceptance/pat/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitkeep
|
0
login/apps/login-test-acceptance/pat/.gitkeep
Normal file
0
login/apps/login-test-acceptance/pat/.gitkeep
Normal file
2
login/apps/login-test-acceptance/playwright-report/.gitignore
vendored
Normal file
2
login/apps/login-test-acceptance/playwright-report/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitkeep
|
78
login/apps/login-test-acceptance/playwright.config.ts
Normal file
78
login/apps/login-test-acceptance/playwright.config.ts
Normal file
@@ -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' },
|
||||
// },
|
||||
],
|
||||
});
|
18
login/apps/login-test-acceptance/samlsp/go.mod
Normal file
18
login/apps/login-test-acceptance/samlsp/go.mod
Normal file
@@ -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
|
||||
)
|
38
login/apps/login-test-acceptance/samlsp/go.sum
Normal file
38
login/apps/login-test-acceptance/samlsp/go.sum
Normal file
@@ -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=
|
271
login/apps/login-test-acceptance/samlsp/main.go
Normal file
271
login/apps/login-test-acceptance/samlsp/main.go
Normal file
@@ -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
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user