mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 17:57:33 +00:00
Add 'login/' from commit '62b43ef0d44e49de38e4018007333bb6dc61d6d6'
git-subtree-dir: login git-subtree-mainline:2b20d8bf35
git-subtree-split:62b43ef0d4
This commit is contained in:
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.
|
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_owner == 'zitadel'
|
||||||
|
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_owner == 'zitadel'
|
||||||
|
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_owner != 'zitadel'
|
||||||
|
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 }}
|
69
login/.github/workflows/test.yml
vendored
Normal file
69
login/.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Quality
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force:
|
||||||
|
description: 'Whether to ignore the run caches'
|
||||||
|
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 == 'push' && github.repository_owner != 'zitadel') ||
|
||||||
|
(github.event_name == 'pull_request' && github.repository_owner != 'zitadel')
|
||||||
|
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:
|
||||||
|
FORCE: ${{ github.event.inputs.force == '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
|
8
login/.prettierignore
Normal file
8
login/.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.next/
|
||||||
|
.changeset/
|
||||||
|
.github/
|
||||||
|
dist/
|
||||||
|
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.
|
143
login/CONTRIBUTING.md
Normal file
143
login/CONTRIBUTING.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# 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
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The application is now available at `http://localhost:3000`
|
||||||
|
|
||||||
|
Configure apps/login/.env.local to target the Zitadel instance of your choice.
|
||||||
|
The login app live-reloads on changes, so you can start developing right away.
|
||||||
|
|
||||||
|
### Developing Against Your Local ZITADEL Instance
|
||||||
|
|
||||||
|
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
|
||||||
|
# This command runs all dependencies and overwrites the file ./apps/login/.env.test.local.
|
||||||
|
pnpm test:acceptance:setup
|
||||||
|
|
||||||
|
# As soon as the setup container completed successfully, you are ready to run the login application in live-reloading dev mode
|
||||||
|
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!*.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the Cypress test suite to run the integration tests in interactive mode.
|
||||||
|
First, set up your local test environment.
|
||||||
|
This runs a mock server in docker and the login application in dev mode with live-reloading enabled.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
|
```
|
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.
|
131
login/Makefile
Normal file
131
login/Makefile
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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_COMMON_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_COMMON_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:02617cf17fdde849378c1a6b5254bbfb2745b164
|
||||||
|
export CORE_MOCK_TAG := login-core-mock:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||||
|
|
||||||
|
.PHONY: login-help
|
||||||
|
login-help:
|
||||||
|
@echo "Makefile for the login service"
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " login-help - Show this help message."
|
||||||
|
@echo " login-generate - Generate TypeScript client code from Protobuf definitions."
|
||||||
|
@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. FORCE=true prevents skipping."
|
||||||
|
@echo " login-test-unit - Run unit tests. Tests without any dependencies. FORCE=true prevents skipping."
|
||||||
|
@echo " login-test-integration - Run integration tests. Tests a login production build against a mocked Zitadel core API. FORCE=true prevents skipping."
|
||||||
|
@echo " login-test-acceptance - Run acceptance tests. Tests a login production build with a local Zitadel instance behind a reverse proxy. FORCE=true prevents skipping."
|
||||||
|
@echo " show-run-caches - Show all run caches with image ids and exit codes."
|
||||||
|
@echo " clean-run-caches - Remove all run caches."
|
||||||
|
|
||||||
|
|
||||||
|
login-lint:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-lint
|
||||||
|
|
||||||
|
login-test-unit:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-test-unit
|
||||||
|
|
||||||
|
login-test-integration-build:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) core-mock login-test-integration login-standalone
|
||||||
|
|
||||||
|
login-test-integration-dev: login-test-integration-cleanup
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_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
|
||||||
|
docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml run --rm integration
|
||||||
|
|
||||||
|
login-test-integration-cleanup:
|
||||||
|
docker compose --file $(LOGIN_DIR)apps/login-test-integration/docker-compose.yaml down --volumes
|
||||||
|
|
||||||
|
.PHONY: login-test-integration
|
||||||
|
login-test-integration: login-test-integration-build
|
||||||
|
$(LOGIN_DIR)scripts/run_or_skip.sh login-test-integration-run \
|
||||||
|
"$(LOGIN_TAG) \
|
||||||
|
$(CORE_MOCK_TAG) \
|
||||||
|
$(LOGIN_TEST_INTEGRATION_TAG)"
|
||||||
|
|
||||||
|
login-test-acceptance-build-bake:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) login-test-acceptance login-standalone
|
||||||
|
|
||||||
|
login-test-acceptance-build-compose:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) --load setup sink
|
||||||
|
|
||||||
|
login-test-acceptance-build: login-test-acceptance-build-compose login-test-acceptance-build-bake
|
||||||
|
@:
|
||||||
|
|
||||||
|
login-test-acceptance-env: login-test-acceptance-build-compose login-test-acceptance-cleanup
|
||||||
|
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml run setup
|
||||||
|
|
||||||
|
login-test-acceptance-dev:
|
||||||
|
docker compose --file $(LOGIN_DIR)apps/login-test-acceptance/docker-compose.yaml up zitadel traefik sink
|
||||||
|
|
||||||
|
login-test-acceptance-run: login-test-acceptance-cleanup
|
||||||
|
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:
|
||||||
|
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_OIDCRP_TAG) \
|
||||||
|
$(LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG)"
|
||||||
|
|
||||||
|
.PHONY: login-quality
|
||||||
|
login-quality: login-lint login-test-unit login-test-integration
|
||||||
|
@:
|
||||||
|
|
||||||
|
.PHONY: login-standalone-build
|
||||||
|
login-standalone-build:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) --load login-standalone
|
||||||
|
|
||||||
|
login-standalone-build-tag:
|
||||||
|
@echo -n "$(LOGIN_TAG)"
|
||||||
|
|
||||||
|
typescript-generate:
|
||||||
|
$(LOGIN_BAKE_CLI_WITH_COMMON_ARGS) typescript-proto-client-out
|
||||||
|
|
||||||
|
.PHONY: clean-run-caches
|
||||||
|
clean-run-caches:
|
||||||
|
@echo "Removing cache directory: $(CACHE_DIR)"
|
||||||
|
rm -rf "$(CACHE_DIR)"
|
||||||
|
|
||||||
|
.PHONY: show-run-caches
|
||||||
|
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
|
||||||
|
|
257
login/README.md
Normal file
257
login/README.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# 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
|
||||||
|
- [ ] LDAP
|
||||||
|
- [ ] 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).
|
||||||
|
|
||||||
|
## 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)
|
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
|
58
login/apps/login-test-acceptance/docker-compose-ci.yaml
Normal file
58
login/apps/login-test-acceptance/docker-compose-ci.yaml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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:
|
||||||
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
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:/builder/apps/login/.env.test.local
|
||||||
|
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
|
238
login/apps/login-test-acceptance/docker-compose.yaml
Normal file
238
login/apps/login-test-acceptance/docker-compose.yaml
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
zitadel:
|
||||||
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v3.3.0}"
|
||||||
|
container_name: acceptance-zitadel
|
||||||
|
pull_policy: always
|
||||||
|
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.1 // 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.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/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": "pnpm exec playwright",
|
||||||
|
"test:acceptance:env": "cd ../.. && make login-test-acceptance-env",
|
||||||
|
"test:acceptance:setup": "cd ../.. && make login-test-acceptance-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
|
||||||
|
}
|
3
login/apps/login-test-acceptance/setup/go.mod
Normal file
3
login/apps/login-test-acceptance/setup/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/zitadel/typescript/apps/login-test-acceptance/setup
|
||||||
|
|
||||||
|
go 1.23.3
|
0
login/apps/login-test-acceptance/setup/go.sum
Normal file
0
login/apps/login-test-acceptance/setup/go.sum
Normal file
3
login/apps/login-test-acceptance/setup/main.go
Normal file
3
login/apps/login-test-acceptance/setup/main.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func main() {}
|
139
login/apps/login-test-acceptance/setup/setup.sh
Executable file
139
login/apps/login-test-acceptance/setup/setup.sh
Executable file
@@ -0,0 +1,139 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e pipefail
|
||||||
|
|
||||||
|
PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat}
|
||||||
|
LOGIN_BASE_URL=${LOGIN_BASE_URL:-"http://localhost:3000"}
|
||||||
|
ZITADEL_API_PROTOCOL="${ZITADEL_API_PROTOCOL:-http}"
|
||||||
|
ZITADEL_API_DOMAIN="${ZITADEL_API_DOMAIN:-localhost}"
|
||||||
|
ZITADEL_API_PORT="${ZITADEL_API_PORT:-8080}"
|
||||||
|
ZITADEL_API_URL="${ZITADEL_API_URL:-${ZITADEL_API_PROTOCOL}://${ZITADEL_API_DOMAIN}:${ZITADEL_API_PORT}}"
|
||||||
|
ZITADEL_API_INTERNAL_URL="${ZITADEL_API_INTERNAL_URL:-${ZITADEL_API_URL}}"
|
||||||
|
SINK_EMAIL_INTERNAL_URL="${SINK_EMAIL_INTERNAL_URL:-"http://sink:3333/email"}"
|
||||||
|
SINK_SMS_INTERNAL_URL="${SINK_SMS_INTERNAL_URL:-"http://sink:3333/sms"}"
|
||||||
|
SINK_NOTIFICATION_URL="${SINK_NOTIFICATION_URL:-"http://localhost:3333/notification"}"
|
||||||
|
WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.test.local}
|
||||||
|
|
||||||
|
if [ -z "${PAT}" ]; then
|
||||||
|
echo "Reading PAT from file ${PAT_FILE}"
|
||||||
|
PAT=$(cat ${PAT_FILE})
|
||||||
|
fi
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# ServiceAccount as Login Client
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
SERVICEACCOUNT_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/machine" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
-d "{\"userName\": \"login\", \"name\": \"Login v2\", \"description\": \"Serviceaccount for Login v2\", \"accessTokenType\": \"ACCESS_TOKEN_TYPE_BEARER\"}")
|
||||||
|
echo "Received ServiceAccount response: ${SERVICEACCOUNT_RESPONSE}"
|
||||||
|
|
||||||
|
SERVICEACCOUNT_ID=$(echo ${SERVICEACCOUNT_RESPONSE} | jq -r '. | .userId')
|
||||||
|
echo "Received ServiceAccount ID: ${SERVICEACCOUNT_ID}"
|
||||||
|
|
||||||
|
MEMBER_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/members" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
-d "{\"userId\": \"${SERVICEACCOUNT_ID}\", \"roles\": [\"IAM_LOGIN_CLIENT\"]}")
|
||||||
|
echo "Received Member response: ${MEMBER_RESPONSE}"
|
||||||
|
|
||||||
|
SA_PAT_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/${SERVICEACCOUNT_ID}/pats" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
-d "{\"expirationDate\": \"2519-04-01T08:45:00.000000Z\"}")
|
||||||
|
echo "Received Member response: ${MEMBER_RESPONSE}"
|
||||||
|
|
||||||
|
SA_PAT=$(echo ${SA_PAT_RESPONSE} | jq -r '. | .token')
|
||||||
|
echo "Received ServiceAccount Token: ${SA_PAT}"
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Environment files
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
echo "Writing environment file ${WRITE_ENVIRONMENT_FILE}."
|
||||||
|
|
||||||
|
echo "ZITADEL_API_URL=${ZITADEL_API_URL}
|
||||||
|
ZITADEL_SERVICE_USER_TOKEN=${SA_PAT}
|
||||||
|
ZITADEL_ADMIN_TOKEN=${PAT}
|
||||||
|
SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
|
||||||
|
EMAIL_VERIFICATION=true
|
||||||
|
DEBUG=false
|
||||||
|
LOGIN_BASE_URL=${LOGIN_BASE_URL}
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
ZITADEL_ADMIN_USER=${ZITADEL_ADMIN_USER:-"zitadel-admin@zitadel.localhost"}
|
||||||
|
NEXT_PUBLIC_BASE_PATH=/ui/v2/login
|
||||||
|
" > ${WRITE_ENVIRONMENT_FILE}
|
||||||
|
|
||||||
|
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"
|
||||||
|
cat ${WRITE_ENVIRONMENT_FILE}
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# SMS provider with HTTP
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
SMSHTTP_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/http" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
-d "{\"endpoint\": \"${SINK_SMS_INTERNAL_URL}\", \"description\": \"test\"}")
|
||||||
|
echo "Received SMS HTTP response: ${SMSHTTP_RESPONSE}"
|
||||||
|
|
||||||
|
SMSHTTP_ID=$(echo ${SMSHTTP_RESPONSE} | jq -r '. | .id')
|
||||||
|
echo "Received SMS HTTP ID: ${SMSHTTP_ID}"
|
||||||
|
|
||||||
|
SMS_ACTIVE_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/${SMSHTTP_ID}/_activate" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json")
|
||||||
|
echo "Received SMS active response: ${SMS_ACTIVE_RESPONSE}"
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Email provider with HTTP
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
EMAILHTTP_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/http" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
-d "{\"endpoint\": \"${SINK_EMAIL_INTERNAL_URL}\", \"description\": \"test\"}")
|
||||||
|
echo "Received Email HTTP response: ${EMAILHTTP_RESPONSE}"
|
||||||
|
|
||||||
|
EMAILHTTP_ID=$(echo ${EMAILHTTP_RESPONSE} | jq -r '. | .id')
|
||||||
|
echo "Received Email HTTP ID: ${EMAILHTTP_ID}"
|
||||||
|
|
||||||
|
EMAIL_ACTIVE_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/${EMAILHTTP_ID}/_activate" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json")
|
||||||
|
echo "Received Email active response: ${EMAIL_ACTIVE_RESPONSE}"
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Wait for projection of default organization in ZITADEL
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
DEFAULTORG_RESPONSE_RESULTS=0
|
||||||
|
# waiting for default organization
|
||||||
|
until [ ${DEFAULTORG_RESPONSE_RESULTS} -eq 1 ]
|
||||||
|
do
|
||||||
|
DEFAULTORG_RESPONSE=$(curl -s --request POST \
|
||||||
|
--url "${ZITADEL_API_INTERNAL_URL}/v2/organizations/_search" \
|
||||||
|
--header "Authorization: Bearer ${PAT}" \
|
||||||
|
--header "Host: ${ZITADEL_API_DOMAIN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
-d "{\"queries\": [{\"defaultQuery\":{}}]}" )
|
||||||
|
echo "Received default organization response: ${DEFAULTORG_RESPONSE}"
|
||||||
|
DEFAULTORG_RESPONSE_RESULTS=$(echo $DEFAULTORG_RESPONSE | jq -r '.result | length')
|
||||||
|
echo "Received default organization response result: ${DEFAULTORG_RESPONSE_RESULTS}"
|
||||||
|
done
|
||||||
|
|
3
login/apps/login-test-acceptance/sink/go.mod
Normal file
3
login/apps/login-test-acceptance/sink/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/zitadel/typescript/acceptance/sink
|
||||||
|
|
||||||
|
go 1.24.0
|
0
login/apps/login-test-acceptance/sink/go.sum
Normal file
0
login/apps/login-test-acceptance/sink/go.sum
Normal file
111
login/apps/login-test-acceptance/sink/main.go
Normal file
111
login/apps/login-test-acceptance/sink/main.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serializableData struct {
|
||||||
|
ContextInfo map[string]interface{} `json:"contextInfo,omitempty"`
|
||||||
|
Args map[string]interface{} `json:"args,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Recipient string `json:"recipient,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := flag.String("port", "3333", "used port for the sink")
|
||||||
|
email := flag.String("email", "/email", "path for a sent email")
|
||||||
|
emailKey := flag.String("email-key", "recipientEmailAddress", "value in the sent context info of the email used as key to retrieve the notification")
|
||||||
|
sms := flag.String("sms", "/sms", "path for a sent sms")
|
||||||
|
smsKey := flag.String("sms-key", "recipientPhoneNumber", "value in the sent context info of the sms used as key to retrieve the notification")
|
||||||
|
notification := flag.String("notification", "/notification", "path to receive the notification")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
messages := make(map[string]serializableData)
|
||||||
|
|
||||||
|
http.HandleFunc(*email, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serializableData := serializableData{}
|
||||||
|
if err := json.Unmarshal(data, &serializableData); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email, ok := serializableData.ContextInfo[*emailKey].(string)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(email + ": " + string(data))
|
||||||
|
messages[email] = serializableData
|
||||||
|
io.WriteString(w, "Email!\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc(*sms, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serializableData := serializableData{}
|
||||||
|
if err := json.Unmarshal(data, &serializableData); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
phone, ok := serializableData.ContextInfo[*smsKey].(string)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(phone + ": " + string(data))
|
||||||
|
messages[phone] = serializableData
|
||||||
|
io.WriteString(w, "SMS!\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc(*notification, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := response{}
|
||||||
|
if err := json.Unmarshal(data, &response); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg, ok := messages[response.Recipient]
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "No messages found for recipient: "+response.Recipient, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serializableData, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
io.WriteString(w, string(serializableData))
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println("Starting server on", *port)
|
||||||
|
fmt.Println(*email, " for email handling")
|
||||||
|
fmt.Println(*sms, " for sms handling")
|
||||||
|
fmt.Println(*notification, " for retrieving notifications")
|
||||||
|
http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return }))
|
||||||
|
fmt.Println("/healthy returns 200 OK")
|
||||||
|
err := http.ListenAndServe(":"+*port, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic("Server could not be started: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
2
login/apps/login-test-acceptance/test-results/.gitignore
vendored
Normal file
2
login/apps/login-test-acceptance/test-results/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitkeep
|
7
login/apps/login-test-acceptance/tests/admin.spec.ts
Normal file
7
login/apps/login-test-acceptance/tests/admin.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { test } from "@playwright/test";
|
||||||
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
|
|
||||||
|
test("admin login", async ({ page }) => {
|
||||||
|
await loginWithPassword(page, process.env["ZITADEL_ADMIN_USER"], "Password1!");
|
||||||
|
await loginScreenExpect(page, "ZITADEL Admin");
|
||||||
|
});
|
12
login/apps/login-test-acceptance/tests/code-screen.ts
Normal file
12
login/apps/login-test-acceptance/tests/code-screen.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const codeTextInput = "code-text-input";
|
||||||
|
|
||||||
|
export async function codeScreen(page: Page, code: string) {
|
||||||
|
await page.getByTestId(codeTextInput).pressSequentially(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function codeScreenExpect(page: Page, code: string) {
|
||||||
|
await expect(page.getByTestId(codeTextInput)).toHaveValue(code);
|
||||||
|
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify OTP code");
|
||||||
|
}
|
17
login/apps/login-test-acceptance/tests/code.ts
Normal file
17
login/apps/login-test-acceptance/tests/code.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { codeScreen } from "./code-screen";
|
||||||
|
import { getOtpFromSink } from "./sink";
|
||||||
|
|
||||||
|
export async function otpFromSink(page: Page, key: string) {
|
||||||
|
const c = await getOtpFromSink(key);
|
||||||
|
await code(page, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function code(page: Page, code: string) {
|
||||||
|
await codeScreen(page, code);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function codeResend(page: Page) {
|
||||||
|
await page.getByTestId("resend-button").click();
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const codeTextInput = "code-text-input";
|
||||||
|
|
||||||
|
export async function emailVerifyScreen(page: Page, code: string) {
|
||||||
|
await page.getByTestId(codeTextInput).pressSequentially(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function emailVerifyScreenExpect(page: Page, code: string) {
|
||||||
|
await expect(page.getByTestId(codeTextInput)).toHaveValue(code);
|
||||||
|
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify email");
|
||||||
|
}
|
69
login/apps/login-test-acceptance/tests/email-verify.spec.ts
Normal file
69
login/apps/login-test-acceptance/tests/email-verify.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { emailVerify, emailVerifyResend } from "./email-verify";
|
||||||
|
import { emailVerifyScreenExpect } from "./email-verify-screen";
|
||||||
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
|
import { getCodeFromSink } from "./sink";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: false,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, verify", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
const c = await getCodeFromSink(user.getUsername());
|
||||||
|
await emailVerify(page, c);
|
||||||
|
// wait for resend of the code
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, resend, verify", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
// auto-redirect on /verify
|
||||||
|
await emailVerifyResend(page);
|
||||||
|
const c = await getCodeFromSink(user.getUsername());
|
||||||
|
// wait for resend of the code
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await emailVerify(page, c);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, resend, old code", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
const c = await getCodeFromSink(user.getUsername());
|
||||||
|
await emailVerifyResend(page);
|
||||||
|
// wait for resend of the code
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await emailVerify(page, c);
|
||||||
|
await emailVerifyScreenExpect(page, c);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user email not verified, wrong code", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
// auto-redirect on /verify
|
||||||
|
const code = "wrong";
|
||||||
|
await emailVerify(page, code);
|
||||||
|
await emailVerifyScreenExpect(page, code);
|
||||||
|
});
|
15
login/apps/login-test-acceptance/tests/email-verify.ts
Normal file
15
login/apps/login-test-acceptance/tests/email-verify.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { emailVerifyScreen } from "./email-verify-screen";
|
||||||
|
|
||||||
|
export async function startEmailVerify(page: Page, loginname: string) {
|
||||||
|
await page.goto("./verify");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function emailVerify(page: Page, code: string) {
|
||||||
|
await emailVerifyScreen(page, code);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function emailVerifyResend(page: Page) {
|
||||||
|
await page.getByTestId("resend-button").click();
|
||||||
|
}
|
102
login/apps/login-test-acceptance/tests/idp-apple.spec.ts
Normal file
102
login/apps/login-test-acceptance/tests/idp-apple.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Note for all tests, in case Apple doesn't deliver all relevant information per default
|
||||||
|
// We should add an action in the needed cases
|
||||||
|
|
||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with Apple IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given an Apple IDP is configured on the organization
|
||||||
|
// Given the user has an Apple added as auth method
|
||||||
|
// User authenticates with Apple
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given an Apple IDP is configured on the organization
|
||||||
|
// Given the user has an Apple added as auth method
|
||||||
|
// User is redirected to Apple
|
||||||
|
// User authenticates with Apple and gets an error
|
||||||
|
// User is redirect back to login
|
||||||
|
// An error is shown to the user "Something went wrong in Apple Login"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Apple is configure on the organization as only authencation method
|
||||||
|
// Given idp Apple is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Apple
|
||||||
|
// User authenticates in Apple
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Apple is configure on the organization as only authencation method
|
||||||
|
// Given idp Apple is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Apple
|
||||||
|
// User authenticates in Apple
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Apple is configure on the organization as only authencation method
|
||||||
|
// Given idp Apple is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Apple
|
||||||
|
// User authenticates in Apple
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Apple is configure on the organization as only authencation method
|
||||||
|
// Given idp Apple is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Apple
|
||||||
|
// User authenticates in Apple with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Apple is configure on the organization as only authencation method
|
||||||
|
// Given idp Apple is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Apple
|
||||||
|
// User authenticates in Apple with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Apple IDP, no user linked, user link successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Apple is configure on the organization as only authencation method
|
||||||
|
// Given idp Apple is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Apple
|
||||||
|
// User authenticates in Apple with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,99 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a Generic JWT IDP is configured on the organization
|
||||||
|
// Given the user has Generic JWT IDP added as auth method
|
||||||
|
// User authenticates with the Generic JWT IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the Generic JWT IDP is configured on the organization
|
||||||
|
// Given the user has Generic JWT IDP added as auth method
|
||||||
|
// User is redirected to the Generic JWT IDP
|
||||||
|
// User authenticates with the Generic JWT IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic JWT is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic JWT
|
||||||
|
// User authenticates in Generic JWT
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic JWT is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic JWT
|
||||||
|
// User authenticates in Generic JWT
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic JWT is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic JWT is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic JWT
|
||||||
|
// User authenticates in Generic JWT
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic JWT is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic JWT is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Generic JWT
|
||||||
|
// User authenticates in Generic JWT with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic JWT is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic JWT is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Generic JWT
|
||||||
|
// User authenticates in Generic JWT with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic JWT IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic JWT is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic JWT is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Generic JWT
|
||||||
|
// User authenticates in Generic JWT with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,99 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a Generic OAuth IDP is configured on the organization
|
||||||
|
// Given the user has Generic OAuth IDP added as auth method
|
||||||
|
// User authenticates with the Generic OAuth IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the Generic OAuth IDP is configured on the organization
|
||||||
|
// Given the user has Generic OAuth IDP added as auth method
|
||||||
|
// User is redirected to the Generic OAuth IDP
|
||||||
|
// User authenticates with the Generic OAuth IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OAuth is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic OAuth
|
||||||
|
// User authenticates in Generic OAuth
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OAuth is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic OAuth
|
||||||
|
// User authenticates in Generic OAuth
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OAuth is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OAuth is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic OAuth
|
||||||
|
// User authenticates in Generic OAuth
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OAuth is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OAuth is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Generic OAuth
|
||||||
|
// User authenticates in Generic OAuth with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OAuth is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OAuth is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Generic OAuth
|
||||||
|
// User authenticates in Generic OAuth with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OAuth IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OAuth is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OAuth is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Generic OAuth
|
||||||
|
// User authenticates in Generic OAuth with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
101
login/apps/login-test-acceptance/tests/idp-generic-oidc.spec.ts
Normal file
101
login/apps/login-test-acceptance/tests/idp-generic-oidc.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Note, we should use a provider such as Google to test this, where we know OIDC standard is properly implemented
|
||||||
|
|
||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a Generic OIDC IDP is configured on the organization
|
||||||
|
// Given the user has Generic OIDC IDP added as auth method
|
||||||
|
// User authenticates with the Generic OIDC IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the Generic OIDC IDP is configured on the organization
|
||||||
|
// Given the user has Generic OIDC IDP added as auth method
|
||||||
|
// User is redirected to the Generic OIDC IDP
|
||||||
|
// User authenticates with the Generic OIDC IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OIDC is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic OIDC
|
||||||
|
// User authenticates in Generic OIDC
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OIDC is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic OIDC
|
||||||
|
// User authenticates in Generic OIDC
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OIDC is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OIDC is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Generic OIDC
|
||||||
|
// User authenticates in Generic OIDC
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OIDC is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OIDC is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Generic OIDC
|
||||||
|
// User authenticates in Generic OIDC with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OIDC is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OIDC is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Generic OIDC
|
||||||
|
// User authenticates in Generic OIDC with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Generic OIDC IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Generic OIDC is configure on the organization as only authencation method
|
||||||
|
// Given idp Generic OIDC is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Generic OIDC
|
||||||
|
// User authenticates in Generic OIDC with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,103 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a GitHub Enterprise IDP is configured on the organization
|
||||||
|
// Given the user has GitHub Enterprise IDP added as auth method
|
||||||
|
// User authenticates with the GitHub Enterprise IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the GitHub Enterprise IDP is configured on the organization
|
||||||
|
// Given the user has GitHub Enterprise IDP added as auth method
|
||||||
|
// User is redirected to the GitHub Enterprise IDP
|
||||||
|
// User authenticates with the GitHub Enterprise IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub Enterprise is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to GitHub Enterprise
|
||||||
|
// User authenticates in GitHub Enterprise
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub Enterprise is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to GitHub Enterprise
|
||||||
|
// User authenticates in GitHub Enterprise
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub Enterprise is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub Enterprise is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to GitHub Enterprise
|
||||||
|
// User authenticates in GitHub Enterprise
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub Enterprise is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub Enterprise is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to GitHub Enterprise
|
||||||
|
// User authenticates in GitHub Enterprise with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub Enterprise is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub Enterprise is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to GitHub Enterprise
|
||||||
|
// User authenticates in GitHub Enterprise with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub Enterprise IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub Enterprise is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub Enterprise is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to GitHub Enterprise
|
||||||
|
// User authenticates in GitHub Enterprise with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
103
login/apps/login-test-acceptance/tests/idp-github.spec.ts
Normal file
103
login/apps/login-test-acceptance/tests/idp-github.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with GitHub IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a GitHub IDP is configured on the organization
|
||||||
|
// Given the user has GitHub IDP added as auth method
|
||||||
|
// User authenticates with the GitHub IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the GitHub IDP is configured on the organization
|
||||||
|
// Given the user has GitHub IDP added as auth method
|
||||||
|
// User is redirected to the GitHub IDP
|
||||||
|
// User authenticates with the GitHub IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to GitHub
|
||||||
|
// User authenticates in GitHub
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to GitHub
|
||||||
|
// User authenticates in GitHub
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to GitHub
|
||||||
|
// User authenticates in GitHub
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to GitHub
|
||||||
|
// User authenticates in GitHub with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to GitHub
|
||||||
|
// User authenticates in GitHub with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitHub IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp GitHub is configure on the organization as only authencation method
|
||||||
|
// Given idp GitHub is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to GitHub
|
||||||
|
// User authenticates in GitHub with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,103 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with GitLab Self-Hosted IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a GitLab Self-Hosted IDP is configured on the organization
|
||||||
|
// Given the user has GitLab Self-Hosted IDP added as auth method
|
||||||
|
// User authenticates with the GitLab Self-Hosted IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitLab Self-Hosted IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the GitLab Self-Hosted IDP is configured on the organization
|
||||||
|
// Given the user has GitLab Self-Hosted IDP added as auth method
|
||||||
|
// User is redirected to the GitLab Self-Hosted IDP
|
||||||
|
// User authenticates with the GitLab Self-Hosted IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab Self-Hosted IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Gitlab Self-Hosted
|
||||||
|
// User authenticates in Gitlab Self-Hosted
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab Self-Hosted IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Gitlab Self-Hosted
|
||||||
|
// User authenticates in Gitlab Self-Hosted
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab Self-Hosted IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab Self-Hosted is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Gitlab Self-Hosted
|
||||||
|
// User authenticates in Gitlab Self-Hosted
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab Self-Hosted IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab Self-Hosted is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Gitlab Self-Hosted
|
||||||
|
// User authenticates in Gitlab Self-Hosted with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab Self-Hosted IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab Self-Hosted is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Gitlab Self-Hosted
|
||||||
|
// User authenticates in Gitlab Self-Hosted with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab Self-Hosted IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab Self-Hosted is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab Self-Hosted is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Gitlab Self-Hosted
|
||||||
|
// User authenticates in Gitlab Self-Hosted with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
103
login/apps/login-test-acceptance/tests/idp-gitlab.spec.ts
Normal file
103
login/apps/login-test-acceptance/tests/idp-gitlab.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with GitLab IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a GitLab IDP is configured on the organization
|
||||||
|
// Given the user has GitLab IDP added as auth method
|
||||||
|
// User authenticates with the GitLab IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with GitLab IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the GitLab IDP is configured on the organization
|
||||||
|
// Given the user has GitLab IDP added as auth method
|
||||||
|
// User is redirected to the GitLab IDP
|
||||||
|
// User authenticates with the GitLab IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Gitlab
|
||||||
|
// User authenticates in Gitlab
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Gitlab
|
||||||
|
// User authenticates in Gitlab
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Gitlab
|
||||||
|
// User authenticates in Gitlab
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Gitlab
|
||||||
|
// User authenticates in Gitlab with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Gitlab
|
||||||
|
// User authenticates in Gitlab with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Gitlab IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Gitlab is configure on the organization as only authencation method
|
||||||
|
// Given idp Gitlab is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Gitlab
|
||||||
|
// User authenticates in Gitlab with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
99
login/apps/login-test-acceptance/tests/idp-google.spec.ts
Normal file
99
login/apps/login-test-acceptance/tests/idp-google.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with Google IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a Google IDP is configured on the organization
|
||||||
|
// Given the user has Google IDP added as auth method
|
||||||
|
// User authenticates with the Google IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the Google IDP is configured on the organization
|
||||||
|
// Given the user has Google IDP added as auth method
|
||||||
|
// User is redirected to the Google IDP
|
||||||
|
// User authenticates with the Google IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Google is configure on the organization as only authencation method
|
||||||
|
// Given idp Google is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Google
|
||||||
|
// User authenticates in Google
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Google is configure on the organization as only authencation method
|
||||||
|
// Given idp Google is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Google
|
||||||
|
// User authenticates in Google
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Google is configure on the organization as only authencation method
|
||||||
|
// Given idp Google is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Google
|
||||||
|
// User authenticates in Google
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Google is configure on the organization as only authencation method
|
||||||
|
// Given idp Google is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Google
|
||||||
|
// User authenticates in Google with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Google is configure on the organization as only authencation method
|
||||||
|
// Given idp Google is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Google
|
||||||
|
// User authenticates in Google with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Google IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Google is configure on the organization as only authencation method
|
||||||
|
// Given idp Google is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Google
|
||||||
|
// User authenticates in Google with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
99
login/apps/login-test-acceptance/tests/idp-ldap.spec.ts
Normal file
99
login/apps/login-test-acceptance/tests/idp-ldap.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with LDAP IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a LDAP IDP is configured on the organization
|
||||||
|
// Given the user has LDAP IDP added as auth method
|
||||||
|
// User authenticates with the LDAP IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the LDAP IDP is configured on the organization
|
||||||
|
// Given the user has LDAP IDP added as auth method
|
||||||
|
// User is redirected to the LDAP IDP
|
||||||
|
// User authenticates with the LDAP IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp LDAP is configure on the organization as only authencation method
|
||||||
|
// Given idp LDAP is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to LDAP
|
||||||
|
// User authenticates in LDAP
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp LDAP is configure on the organization as only authencation method
|
||||||
|
// Given idp LDAP is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to LDAP
|
||||||
|
// User authenticates in LDAP
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp LDAP is configure on the organization as only authencation method
|
||||||
|
// Given idp LDAP is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to LDAP
|
||||||
|
// User authenticates in LDAP
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp LDAP is configure on the organization as only authencation method
|
||||||
|
// Given idp LDAP is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to LDAP
|
||||||
|
// User authenticates in LDAP with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp LDAP is configure on the organization as only authencation method
|
||||||
|
// Given idp LDAP is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to LDAP
|
||||||
|
// User authenticates in LDAP with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with LDAP IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp LDAP is configure on the organization as only authencation method
|
||||||
|
// Given idp LDAP is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to LDAP
|
||||||
|
// User authenticates in LDAP with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
102
login/apps/login-test-acceptance/tests/idp-microsoft.spec.ts
Normal file
102
login/apps/login-test-acceptance/tests/idp-microsoft.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Note for all tests, in case Microsoft doesn't deliver all relevant information per default
|
||||||
|
// We should add an action in the needed cases
|
||||||
|
|
||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with Microsoft IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a Microsoft IDP is configured on the organization
|
||||||
|
// Given the user has Microsoft IDP added as auth method
|
||||||
|
// User authenticates with the Microsoft IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the Microsoft IDP is configured on the organization
|
||||||
|
// Given the user has Microsoft IDP added as auth method
|
||||||
|
// User is redirected to the Microsoft IDP
|
||||||
|
// User authenticates with the Microsoft IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Microsoft is configure on the organization as only authencation method
|
||||||
|
// Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Microsoft
|
||||||
|
// User authenticates in Microsoft
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Microsoft is configure on the organization as only authencation method
|
||||||
|
// Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Microsoft
|
||||||
|
// User authenticates in Microsoft
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Microsoft is configure on the organization as only authencation method
|
||||||
|
// Given idp Microsoft is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to Microsoft
|
||||||
|
// User authenticates in Microsoft
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Microsoft is configure on the organization as only authencation method
|
||||||
|
// Given idp Microsoft is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to Microsoft
|
||||||
|
// User authenticates in Microsoft with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Microsoft is configure on the organization as only authencation method
|
||||||
|
// Given idp Microsoft is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Microsoft
|
||||||
|
// User authenticates in Microsoft with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with Microsoft IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp Microsoft is configure on the organization as only authencation method
|
||||||
|
// Given idp Microsoft is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to Microsoft
|
||||||
|
// User authenticates in Microsoft with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
103
login/apps/login-test-acceptance/tests/idp-saml.spec.ts
Normal file
103
login/apps/login-test-acceptance/tests/idp-saml.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with SAML IDP", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given a SAML IDP is configured on the organization
|
||||||
|
// Given the user has SAML IDP added as auth method
|
||||||
|
// User authenticates with the SAML IDP
|
||||||
|
// User is redirected back to login
|
||||||
|
// User is redirected to the app
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP - error", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the SAML IDP is configured on the organization
|
||||||
|
// Given the user has SAML IDP added as auth method
|
||||||
|
// User is redirected to the SAML IDP
|
||||||
|
// User authenticates with the SAML IDP and gets an error
|
||||||
|
// User is redirected back to login
|
||||||
|
// An error is shown to the user "Something went wrong"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP, no user existing - auto register", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp SAML is configure on the organization as only authencation method
|
||||||
|
// Given idp SAML is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to SAML
|
||||||
|
// User authenticates in SAML
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP, no user existing - auto register not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp SAML is configure on the organization as only authencation method
|
||||||
|
// Given idp SAML is configure with account creation alloweed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to SAML
|
||||||
|
// User authenticates in SAML
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// User will see the registration page with pre filled user information
|
||||||
|
// User fills missing information
|
||||||
|
// User clicks register button
|
||||||
|
// User is created in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp SAML is configure on the organization as only authencation method
|
||||||
|
// Given idp SAML is configure with account creation not allowed, and automatic creation enabled
|
||||||
|
// Given no user exists yet
|
||||||
|
// User is automatically redirected to SAML
|
||||||
|
// User authenticates in SAML
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// Because of missing informaiton on the user auto creation is not possible
|
||||||
|
// Error message is shown, that registration of the user was not possible due to missing information
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP, no user linked - auto link", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp SAML is configure on the organization as only authencation method
|
||||||
|
// Given idp SAML is configure with account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com exists
|
||||||
|
// User is automatically redirected to SAML
|
||||||
|
// User authenticates in SAML with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User is linked with existing user in ZITADEL
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP, no user linked, linking not possible", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp SAML is configure on the organization as only authencation method
|
||||||
|
// Given idp SAML is configure with manually account linking not allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to SAML
|
||||||
|
// User authenticates in SAML with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User will get an error message that account linking wasn't possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with SAML IDP, no user linked, linking successful", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given idp SAML is configure on the organization as only authencation method
|
||||||
|
// Given idp SAML is configure with manually account linking allowed, and linking set to existing email
|
||||||
|
// Given ZITADEL Action is added to autofill missing user information
|
||||||
|
// Given user with email address user@zitadel.com doesn't exists
|
||||||
|
// User is automatically redirected to SAML
|
||||||
|
// User authenticates in SAML with user@zitadel.com
|
||||||
|
// User is redirect to ZITADEL login
|
||||||
|
// User with email address user@zitadel.com can not be found
|
||||||
|
// User is prompted to link the account manually
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,57 @@
|
|||||||
|
import test from "@playwright/test";
|
||||||
|
|
||||||
|
test("login with mfa setup, mfa setup prompt", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has enabled at least one mfa types
|
||||||
|
// Given the user has a password but no mfa registered
|
||||||
|
// User authenticates with login name and password
|
||||||
|
// User is prompted to setup a mfa, mfa providers are listed, the user can choose the provider
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with mfa setup, no mfa setup prompt", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has set "multifactor init check time" to 0
|
||||||
|
// Given the organization has enabled mfa types
|
||||||
|
// Given the user has a password but no mfa registered
|
||||||
|
// User authenticates with loginname and password
|
||||||
|
// user is directly loged in and not prompted to setup mfa
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with mfa setup, force mfa for local authenticated users", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has enabled force mfa for local authentiacted users
|
||||||
|
// Given the organization has enabled all possible mfa types
|
||||||
|
// Given the user has a password but no mfa registered
|
||||||
|
// User authenticates with loginname and password
|
||||||
|
// User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with mfa setup, force mfa - local user", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has enabled force mfa for local authentiacted users
|
||||||
|
// Given the organization has enabled all possible mfa types
|
||||||
|
// Given the user has a password but no mfa registered
|
||||||
|
// User authenticates with loginname and password
|
||||||
|
// User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with mfa setup, force mfa - external user", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has enabled force mfa
|
||||||
|
// Given the organization has enabled all possible mfa types
|
||||||
|
// Given the user has an idp but no mfa registered
|
||||||
|
// enter login name
|
||||||
|
// redirect to configured external idp
|
||||||
|
// User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login with mfa setup, force mfa - local user, wrong password", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has a password lockout policy set to 1 on the max password attempts
|
||||||
|
// Given the user has only a password as auth methos
|
||||||
|
// enter login name
|
||||||
|
// enter wrong password
|
||||||
|
// User will get an error "Wrong password"
|
||||||
|
// enter password
|
||||||
|
// User will get an error "Max password attempts reached - user is locked. Please reach out to your administrator"
|
||||||
|
});
|
41
login/apps/login-test-acceptance/tests/login.ts
Normal file
41
login/apps/login-test-acceptance/tests/login.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
import { code, otpFromSink } from "./code";
|
||||||
|
import { loginname } from "./loginname";
|
||||||
|
import { password } from "./password";
|
||||||
|
import { totp } from "./zitadel";
|
||||||
|
|
||||||
|
export async function startLogin(page: Page) {
|
||||||
|
await page.goto(`./loginname`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithPassword(page: Page, username: string, pw: string) {
|
||||||
|
await startLogin(page);
|
||||||
|
await loginname(page, username);
|
||||||
|
await password(page, pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithPasskey(page: Page, authenticatorId: string, username: string) {
|
||||||
|
await startLogin(page);
|
||||||
|
await loginname(page, username);
|
||||||
|
// await passkey(page, authenticatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginScreenExpect(page: Page, fullName: string) {
|
||||||
|
await expect(page).toHaveURL(/.*signedin.*/);
|
||||||
|
await expect(page.getByRole("heading")).toContainText(fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithPasswordAndEmailOTP(page: Page, username: string, password: string, email: string) {
|
||||||
|
await loginWithPassword(page, username, password);
|
||||||
|
await otpFromSink(page, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithPasswordAndPhoneOTP(page: Page, username: string, password: string, phone: string) {
|
||||||
|
await loginWithPassword(page, username, password);
|
||||||
|
await otpFromSink(page, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithPasswordAndTOTP(page: Page, username: string, password: string, secret: string) {
|
||||||
|
await loginWithPassword(page, username, password);
|
||||||
|
await code(page, totp(secret));
|
||||||
|
}
|
12
login/apps/login-test-acceptance/tests/loginname-screen.ts
Normal file
12
login/apps/login-test-acceptance/tests/loginname-screen.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const usernameTextInput = "username-text-input";
|
||||||
|
|
||||||
|
export async function loginnameScreen(page: Page, username: string) {
|
||||||
|
await page.getByTestId(usernameTextInput).pressSequentially(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginnameScreenExpect(page: Page, username: string) {
|
||||||
|
await expect(page.getByTestId(usernameTextInput)).toHaveValue(username);
|
||||||
|
await expect(page.getByTestId("error").locator("div")).toContainText("User not found in the system");
|
||||||
|
}
|
7
login/apps/login-test-acceptance/tests/loginname.ts
Normal file
7
login/apps/login-test-acceptance/tests/loginname.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { loginnameScreen } from "./loginname-screen";
|
||||||
|
|
||||||
|
export async function loginname(page: Page, username: string) {
|
||||||
|
await loginnameScreen(page, username);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
}
|
109
login/apps/login-test-acceptance/tests/passkey.ts
Normal file
109
login/apps/login-test-acceptance/tests/passkey.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
import { CDPSession } from "playwright-core";
|
||||||
|
|
||||||
|
interface session {
|
||||||
|
client: CDPSession;
|
||||||
|
authenticatorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function client(page: Page): Promise<session> {
|
||||||
|
const cdpSession = await page.context().newCDPSession(page);
|
||||||
|
await cdpSession.send("WebAuthn.enable", { enableUI: false });
|
||||||
|
const result = await cdpSession.send("WebAuthn.addVirtualAuthenticator", {
|
||||||
|
options: {
|
||||||
|
protocol: "ctap2",
|
||||||
|
transport: "internal",
|
||||||
|
hasResidentKey: true,
|
||||||
|
hasUserVerification: true,
|
||||||
|
isUserVerified: true,
|
||||||
|
automaticPresenceSimulation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { client: cdpSession, authenticatorId: result.authenticatorId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function passkeyRegister(page: Page): Promise<string> {
|
||||||
|
const session = await client(page);
|
||||||
|
|
||||||
|
await passkeyNotExisting(session.client, session.authenticatorId);
|
||||||
|
await simulateSuccessfulPasskeyRegister(session.client, session.authenticatorId, () =>
|
||||||
|
page.getByTestId("submit-button").click(),
|
||||||
|
);
|
||||||
|
await passkeyRegistered(session.client, session.authenticatorId);
|
||||||
|
|
||||||
|
return session.authenticatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function passkey(page: Page, authenticatorId: string) {
|
||||||
|
const cdpSession = await page.context().newCDPSession(page);
|
||||||
|
await cdpSession.send("WebAuthn.enable", { enableUI: false });
|
||||||
|
|
||||||
|
const signCount = await passkeyExisting(cdpSession, authenticatorId);
|
||||||
|
|
||||||
|
await simulateSuccessfulPasskeyInput(cdpSession, authenticatorId, () => page.getByTestId("submit-button").click());
|
||||||
|
|
||||||
|
await passkeyUsed(cdpSession, authenticatorId, signCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function passkeyNotExisting(client: CDPSession, authenticatorId: string) {
|
||||||
|
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
|
||||||
|
expect(result.credentials).toHaveLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function passkeyRegistered(client: CDPSession, authenticatorId: string) {
|
||||||
|
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
|
||||||
|
expect(result.credentials).toHaveLength(1);
|
||||||
|
await passkeyUsed(client, authenticatorId, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function passkeyExisting(client: CDPSession, authenticatorId: string): Promise<number> {
|
||||||
|
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
|
||||||
|
expect(result.credentials).toHaveLength(1);
|
||||||
|
return result.credentials[0].signCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function passkeyUsed(client: CDPSession, authenticatorId: string, signCount: number) {
|
||||||
|
const result = await client.send("WebAuthn.getCredentials", { authenticatorId });
|
||||||
|
expect(result.credentials).toHaveLength(1);
|
||||||
|
expect(result.credentials[0].signCount).toBeGreaterThan(signCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateSuccessfulPasskeyRegister(
|
||||||
|
client: CDPSession,
|
||||||
|
authenticatorId: string,
|
||||||
|
operationTrigger: () => Promise<void>,
|
||||||
|
) {
|
||||||
|
// initialize event listeners to wait for a successful passkey input event
|
||||||
|
const operationCompleted = new Promise<void>((resolve) => {
|
||||||
|
client.on("WebAuthn.credentialAdded", () => {
|
||||||
|
console.log("Credential Added!");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// perform a user action that triggers passkey prompt
|
||||||
|
await operationTrigger();
|
||||||
|
|
||||||
|
// wait to receive the event that the passkey was successfully registered or verified
|
||||||
|
await operationCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateSuccessfulPasskeyInput(
|
||||||
|
client: CDPSession,
|
||||||
|
authenticatorId: string,
|
||||||
|
operationTrigger: () => Promise<void>,
|
||||||
|
) {
|
||||||
|
// initialize event listeners to wait for a successful passkey input event
|
||||||
|
const operationCompleted = new Promise<void>((resolve) => {
|
||||||
|
client.on("WebAuthn.credentialAsserted", () => {
|
||||||
|
console.log("Credential Asserted!");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// perform a user action that triggers passkey prompt
|
||||||
|
await operationTrigger();
|
||||||
|
|
||||||
|
// wait to receive the event that the passkey was successfully registered or verified
|
||||||
|
await operationCompleted;
|
||||||
|
}
|
98
login/apps/login-test-acceptance/tests/password-screen.ts
Normal file
98
login/apps/login-test-acceptance/tests/password-screen.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
import { getCodeFromSink } from "./sink";
|
||||||
|
|
||||||
|
const codeField = "code-text-input";
|
||||||
|
const passwordField = "password-text-input";
|
||||||
|
const passwordChangeField = "password-change-text-input";
|
||||||
|
const passwordChangeConfirmField = "password-change-confirm-text-input";
|
||||||
|
const passwordSetField = "password-set-text-input";
|
||||||
|
const passwordSetConfirmField = "password-set-confirm-text-input";
|
||||||
|
const lengthCheck = "length-check";
|
||||||
|
const symbolCheck = "symbol-check";
|
||||||
|
const numberCheck = "number-check";
|
||||||
|
const uppercaseCheck = "uppercase-check";
|
||||||
|
const lowercaseCheck = "lowercase-check";
|
||||||
|
const equalCheck = "equal-check";
|
||||||
|
|
||||||
|
const matchText = "Matches";
|
||||||
|
const noMatchText = "Doesn't match";
|
||||||
|
|
||||||
|
export async function changePasswordScreen(page: Page, password1: string, password2: string) {
|
||||||
|
await page.getByTestId(passwordChangeField).pressSequentially(password1);
|
||||||
|
await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function passwordScreen(page: Page, password: string) {
|
||||||
|
await page.getByTestId(passwordField).pressSequentially(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function passwordScreenExpect(page: Page, password: string) {
|
||||||
|
await expect(page.getByTestId(passwordField)).toHaveValue(password);
|
||||||
|
await expect(page.getByTestId("error").locator("div")).toContainText("Failed to authenticate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePasswordScreenExpect(
|
||||||
|
page: Page,
|
||||||
|
password1: string,
|
||||||
|
password2: string,
|
||||||
|
length: boolean,
|
||||||
|
symbol: boolean,
|
||||||
|
number: boolean,
|
||||||
|
uppercase: boolean,
|
||||||
|
lowercase: boolean,
|
||||||
|
equals: boolean,
|
||||||
|
) {
|
||||||
|
await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1);
|
||||||
|
await expect(page.getByTestId(passwordChangeConfirmField)).toHaveValue(password2);
|
||||||
|
|
||||||
|
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkComplexity(
|
||||||
|
page: Page,
|
||||||
|
length: boolean,
|
||||||
|
symbol: boolean,
|
||||||
|
number: boolean,
|
||||||
|
uppercase: boolean,
|
||||||
|
lowercase: boolean,
|
||||||
|
equals: boolean,
|
||||||
|
) {
|
||||||
|
await checkContent(page, lengthCheck, length);
|
||||||
|
await checkContent(page, symbolCheck, symbol);
|
||||||
|
await checkContent(page, numberCheck, number);
|
||||||
|
await checkContent(page, uppercaseCheck, uppercase);
|
||||||
|
await checkContent(page, lowercaseCheck, lowercase);
|
||||||
|
await checkContent(page, equalCheck, equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkContent(page: Page, testid: string, match: boolean) {
|
||||||
|
if (match) {
|
||||||
|
await expect(page.getByTestId(testid)).toContainText(matchText);
|
||||||
|
} else {
|
||||||
|
await expect(page.getByTestId(testid)).toContainText(noMatchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) {
|
||||||
|
const c = await getCodeFromSink(username);
|
||||||
|
await page.getByTestId(codeField).pressSequentially(c);
|
||||||
|
await page.getByTestId(passwordSetField).pressSequentially(password1);
|
||||||
|
await page.getByTestId(passwordSetConfirmField).pressSequentially(password2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetPasswordScreenExpect(
|
||||||
|
page: Page,
|
||||||
|
password1: string,
|
||||||
|
password2: string,
|
||||||
|
length: boolean,
|
||||||
|
symbol: boolean,
|
||||||
|
number: boolean,
|
||||||
|
uppercase: boolean,
|
||||||
|
lowercase: boolean,
|
||||||
|
equals: boolean,
|
||||||
|
) {
|
||||||
|
await expect(page.getByTestId(passwordSetField)).toHaveValue(password1);
|
||||||
|
await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2);
|
||||||
|
|
||||||
|
await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals);
|
||||||
|
}
|
29
login/apps/login-test-acceptance/tests/password.ts
Normal file
29
login/apps/login-test-acceptance/tests/password.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { changePasswordScreen, passwordScreen, resetPasswordScreen } from "./password-screen";
|
||||||
|
|
||||||
|
const passwordSubmitButton = "submit-button";
|
||||||
|
const passwordResetButton = "reset-button";
|
||||||
|
|
||||||
|
export async function startChangePassword(page: Page, loginname: string) {
|
||||||
|
await page.goto("./password/change?" + new URLSearchParams({ loginName: loginname }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(page: Page, password: string) {
|
||||||
|
await changePasswordScreen(page, password, password);
|
||||||
|
await page.getByTestId(passwordSubmitButton).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function password(page: Page, password: string) {
|
||||||
|
await passwordScreen(page, password);
|
||||||
|
await page.getByTestId(passwordSubmitButton).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startResetPassword(page: Page) {
|
||||||
|
await page.getByTestId(passwordResetButton).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetPassword(page: Page, username: string, password: string) {
|
||||||
|
await startResetPassword(page);
|
||||||
|
await resetPasswordScreen(page, username, password, password);
|
||||||
|
await page.getByTestId(passwordSubmitButton).click();
|
||||||
|
}
|
27
login/apps/login-test-acceptance/tests/register-screen.ts
Normal file
27
login/apps/login-test-acceptance/tests/register-screen.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const passwordField = "password-text-input";
|
||||||
|
const passwordConfirmField = "password-confirm-text-input";
|
||||||
|
|
||||||
|
export async function registerUserScreenPassword(page: Page, firstname: string, lastname: string, email: string) {
|
||||||
|
await registerUserScreen(page, firstname, lastname, email);
|
||||||
|
await page.getByTestId("password-radio").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerUserScreenPasskey(page: Page, firstname: string, lastname: string, email: string) {
|
||||||
|
await registerUserScreen(page, firstname, lastname, email);
|
||||||
|
await page.getByTestId("passkey-radio").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerPasswordScreen(page: Page, password1: string, password2: string) {
|
||||||
|
await page.getByTestId(passwordField).pressSequentially(password1);
|
||||||
|
await page.getByTestId(passwordConfirmField).pressSequentially(password2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerUserScreen(page: Page, firstname: string, lastname: string, email: string) {
|
||||||
|
await page.getByTestId("firstname-text-input").pressSequentially(firstname);
|
||||||
|
await page.getByTestId("lastname-text-input").pressSequentially(lastname);
|
||||||
|
await page.getByTestId("email-text-input").pressSequentially(email);
|
||||||
|
await page.getByTestId("privacy-policy-checkbox").check();
|
||||||
|
await page.getByTestId("tos-checkbox").check();
|
||||||
|
}
|
183
login/apps/login-test-acceptance/tests/register.spec.ts
Normal file
183
login/apps/login-test-acceptance/tests/register.spec.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect } from "./login";
|
||||||
|
import { registerWithPasskey, registerWithPassword } from "./register";
|
||||||
|
import { removeUserByUsername } from "./zitadel";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
test("register with password", async ({ page }) => {
|
||||||
|
const username = faker.internet.email();
|
||||||
|
const password = "Password1!";
|
||||||
|
const firstname = faker.person.firstName();
|
||||||
|
const lastname = faker.person.lastName();
|
||||||
|
|
||||||
|
await registerWithPassword(page, firstname, lastname, username, password, password);
|
||||||
|
await loginScreenExpect(page, firstname + " " + lastname);
|
||||||
|
|
||||||
|
// wait for projection of user
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
await removeUserByUsername(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with passkey", async ({ page }) => {
|
||||||
|
const username = faker.internet.email();
|
||||||
|
const firstname = faker.person.firstName();
|
||||||
|
const lastname = faker.person.lastName();
|
||||||
|
|
||||||
|
await registerWithPasskey(page, firstname, lastname, username);
|
||||||
|
await loginScreenExpect(page, firstname + " " + lastname);
|
||||||
|
|
||||||
|
// wait for projection of user
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
await removeUserByUsername(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - only password enabled", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is not enabled
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// Only password is shown as an option - no passkey
|
||||||
|
// User enters "firstname", "lastname", "username" and "password"
|
||||||
|
// User is redirected to app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - wrong password not enough characters", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is not enabled
|
||||||
|
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// Only password is shown as an option - no passkey
|
||||||
|
// User enters "firstname", "lastname", "username" and a password thats to short
|
||||||
|
// Error is shown "Password doesn't match the policy - it must have at least 8 characters"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - wrong password number missing", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is not enabled
|
||||||
|
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// Only password is shown as an option - no passkey
|
||||||
|
// User enters "firstname", "lastname", "username" and a password without a number
|
||||||
|
// Error is shown "Password doesn't match the policy - number missing"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - wrong password upper case missing", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is not enabled
|
||||||
|
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// Only password is shown as an option - no passkey
|
||||||
|
// User enters "firstname", "lastname", "username" and a password without an upper case
|
||||||
|
// Error is shown "Password doesn't match the policy - uppercase letter missing"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - wrong password lower case missing", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is not enabled
|
||||||
|
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// Only password is shown as an option - no passkey
|
||||||
|
// User enters "firstname", "lastname", "username" and a password without an lower case
|
||||||
|
// Error is shown "Password doesn't match the policy - lowercase letter missing"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - wrong password symboo missing", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is not enabled
|
||||||
|
// Given password policy is set to 8 characters and must include number, symbol, lower and upper letter
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// Only password is shown as an option - no passkey
|
||||||
|
// User enters "firstname", "lastname", "username" and a password without an symbol
|
||||||
|
// Error is shown "Password doesn't match the policy - symbol missing"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - password and passkey enabled", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is enabled
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// User enters "firstname", "lastname", "username"
|
||||||
|
// Password and passkey are shown as authentication option
|
||||||
|
// User clicks password
|
||||||
|
// User enters password
|
||||||
|
// User is redirected to app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and passkey - password and passkey enabled", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given on the default organization passkey is enabled
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration page
|
||||||
|
// User enters "firstname", "lastname", "username"
|
||||||
|
// Password and passkey are shown as authentication option
|
||||||
|
// User clicks passkey
|
||||||
|
// Passkey is opened automatically
|
||||||
|
// User verifies passkey
|
||||||
|
// User is redirected to app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - registration disabled", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization no idp is configured and enabled
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Button "register new user" is not available
|
||||||
|
});
|
||||||
|
|
||||||
|
test("register with username and password - multiple registration options", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given on the default organization "username and password is allowed" is enabled
|
||||||
|
// Given on the default organization "username registeration allowed" is enabled
|
||||||
|
// Given on the default organization one idp is configured and enabled
|
||||||
|
// Given user doesn't exist
|
||||||
|
// Click on button "register new user"
|
||||||
|
// User is redirected to registration options
|
||||||
|
// Local User and idp button are shown
|
||||||
|
// User clicks idp button
|
||||||
|
// User enters "firstname", "lastname", "username" and "password"
|
||||||
|
// User clicks next
|
||||||
|
// User is redirected to app (default redirect url)
|
||||||
|
});
|
39
login/apps/login-test-acceptance/tests/register.ts
Normal file
39
login/apps/login-test-acceptance/tests/register.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { emailVerify } from "./email-verify";
|
||||||
|
import { passkeyRegister } from "./passkey";
|
||||||
|
import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen";
|
||||||
|
import { getCodeFromSink } from "./sink";
|
||||||
|
|
||||||
|
export async function registerWithPassword(
|
||||||
|
page: Page,
|
||||||
|
firstname: string,
|
||||||
|
lastname: string,
|
||||||
|
email: string,
|
||||||
|
password1: string,
|
||||||
|
password2: string,
|
||||||
|
) {
|
||||||
|
await page.goto("./register");
|
||||||
|
await registerUserScreenPassword(page, firstname, lastname, email);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
await registerPasswordScreen(page, password1, password2);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
await verifyEmail(page, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise<string> {
|
||||||
|
await page.goto("./register");
|
||||||
|
await registerUserScreenPasskey(page, firstname, lastname, email);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
|
||||||
|
// wait for projection of user
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
const authId = await passkeyRegister(page);
|
||||||
|
|
||||||
|
await verifyEmail(page, email);
|
||||||
|
return authId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyEmail(page: Page, email: string) {
|
||||||
|
const c = await getCodeFromSink(email);
|
||||||
|
await emailVerify(page, c);
|
||||||
|
}
|
5
login/apps/login-test-acceptance/tests/select-account.ts
Normal file
5
login/apps/login-test-acceptance/tests/select-account.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export async function selectNewAccount(page: Page) {
|
||||||
|
await page.getByRole("link", { name: "Add another account" }).click();
|
||||||
|
}
|
43
login/apps/login-test-acceptance/tests/sink.ts
Normal file
43
login/apps/login-test-acceptance/tests/sink.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Gaxios, GaxiosResponse } from "gaxios";
|
||||||
|
|
||||||
|
const awaitNotification = new Gaxios({
|
||||||
|
url: process.env.SINK_NOTIFICATION_URL,
|
||||||
|
method: "POST",
|
||||||
|
retryConfig: {
|
||||||
|
httpMethodsToRetry: ["POST"],
|
||||||
|
statusCodesToRetry: [[404, 404]],
|
||||||
|
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
|
||||||
|
totalTimeout: 10000, // 10 seconds
|
||||||
|
onRetryAttempt: (error) => {
|
||||||
|
console.warn(`Retrying request to sink notification service: ${error.message}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getOtpFromSink(recipient: string): Promise<any> {
|
||||||
|
return awaitNotification.request({ data: { recipient } }).then((response) => {
|
||||||
|
expectSuccess(response);
|
||||||
|
const otp = response?.data?.args?.otp;
|
||||||
|
if (!otp) {
|
||||||
|
throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
|
||||||
|
}
|
||||||
|
return otp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCodeFromSink(recipient: string): Promise<any> {
|
||||||
|
return awaitNotification.request({ data: { recipient } }).then((response) => {
|
||||||
|
expectSuccess(response);
|
||||||
|
const code = response?.data?.args?.code;
|
||||||
|
if (!code) {
|
||||||
|
throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectSuccess(response: GaxiosResponse): void {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Expected HTTP status 200, but got: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
}
|
177
login/apps/login-test-acceptance/tests/user.ts
Normal file
177
login/apps/login-test-acceptance/tests/user.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { registerWithPasskey } from "./register";
|
||||||
|
import { activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser } from "./zitadel";
|
||||||
|
|
||||||
|
export interface userProps {
|
||||||
|
email: string;
|
||||||
|
isEmailVerified?: boolean;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
organization: string;
|
||||||
|
password: string;
|
||||||
|
passwordChangeRequired?: boolean;
|
||||||
|
phone: string;
|
||||||
|
isPhoneVerified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
private readonly props: userProps;
|
||||||
|
private user: string;
|
||||||
|
|
||||||
|
constructor(userProps: userProps) {
|
||||||
|
this.props = userProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensure(page: Page) {
|
||||||
|
const response = await addUser(this.props);
|
||||||
|
|
||||||
|
this.setUserId(response.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup() {
|
||||||
|
await removeUser(this.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUserId(userId: string) {
|
||||||
|
this.user = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUserId() {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsername() {
|
||||||
|
return this.props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPassword() {
|
||||||
|
return this.props.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFirstname() {
|
||||||
|
return this.props.firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLastname() {
|
||||||
|
return this.props.lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPhone() {
|
||||||
|
return this.props.phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFullName() {
|
||||||
|
return `${this.props.firstName} ${this.props.lastName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasswordUser extends User {
|
||||||
|
async ensure(page: Page) {
|
||||||
|
await super.ensure(page);
|
||||||
|
await eventualNewUser(this.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OtpType {
|
||||||
|
sms = "sms",
|
||||||
|
email = "email",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface otpUserProps {
|
||||||
|
email: string;
|
||||||
|
isEmailVerified?: boolean;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
organization: string;
|
||||||
|
password: string;
|
||||||
|
passwordChangeRequired?: boolean;
|
||||||
|
phone: string;
|
||||||
|
isPhoneVerified?: boolean;
|
||||||
|
type: OtpType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasswordUserWithOTP extends User {
|
||||||
|
private type: OtpType;
|
||||||
|
|
||||||
|
constructor(props: otpUserProps) {
|
||||||
|
super({
|
||||||
|
email: props.email,
|
||||||
|
firstName: props.firstName,
|
||||||
|
lastName: props.lastName,
|
||||||
|
organization: props.organization,
|
||||||
|
password: props.password,
|
||||||
|
phone: props.phone,
|
||||||
|
isEmailVerified: props.isEmailVerified,
|
||||||
|
isPhoneVerified: props.isPhoneVerified,
|
||||||
|
passwordChangeRequired: props.passwordChangeRequired,
|
||||||
|
});
|
||||||
|
this.type = props.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensure(page: Page) {
|
||||||
|
await super.ensure(page);
|
||||||
|
await activateOTP(this.getUserId(), this.type);
|
||||||
|
await eventualNewUser(this.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasswordUserWithTOTP extends User {
|
||||||
|
private secret: string;
|
||||||
|
|
||||||
|
async ensure(page: Page) {
|
||||||
|
await super.ensure(page);
|
||||||
|
this.secret = await addTOTP(this.getUserId());
|
||||||
|
await eventualNewUser(this.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSecret(): string {
|
||||||
|
return this.secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface passkeyUserProps {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
organization: string;
|
||||||
|
phone: string;
|
||||||
|
isEmailVerified?: boolean;
|
||||||
|
isPhoneVerified?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasskeyUser extends User {
|
||||||
|
private authenticatorId: string;
|
||||||
|
|
||||||
|
constructor(props: passkeyUserProps) {
|
||||||
|
super({
|
||||||
|
email: props.email,
|
||||||
|
firstName: props.firstName,
|
||||||
|
lastName: props.lastName,
|
||||||
|
organization: props.organization,
|
||||||
|
password: "",
|
||||||
|
phone: props.phone,
|
||||||
|
isEmailVerified: props.isEmailVerified,
|
||||||
|
isPhoneVerified: props.isPhoneVerified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ensure(page: Page) {
|
||||||
|
const authId = await registerWithPasskey(page, this.getFirstname(), this.getLastname(), this.getUsername());
|
||||||
|
this.authenticatorId = authId;
|
||||||
|
|
||||||
|
// wait for projection of user
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup() {
|
||||||
|
const resp: any = await getUserByUsername(this.getUsername());
|
||||||
|
if (!resp || !resp.result || !resp.result[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await removeUser(resp.result[0].userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAuthenticatorId(): string {
|
||||||
|
return this.authenticatorId;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect, loginWithPasskey } from "./login";
|
||||||
|
import { PasskeyUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasskeyUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasskeyUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and passkey login", async ({ user, page }) => {
|
||||||
|
await loginWithPasskey(page, user.getAuthenticatorId(), user.getUsername());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and passkey login, multiple auth methods", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given passkey and password is enabled on the organization of the user
|
||||||
|
// Given the user has password and passkey registered
|
||||||
|
// enter username
|
||||||
|
// passkey popup is directly shown
|
||||||
|
// user aborts passkey authentication
|
||||||
|
// user switches to password authentication
|
||||||
|
// user enters password
|
||||||
|
// user is redirected to app
|
||||||
|
});
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
|
import { changePassword } from "./password";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: true,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, change required", async ({ user, page }) => {
|
||||||
|
const changedPw = "ChangedPw1!";
|
||||||
|
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
await changePassword(page, changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
|
||||||
|
await loginWithPassword(page, user.getUsername(), changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
@@ -0,0 +1,54 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect, loginWithPassword } from "./login";
|
||||||
|
import { changePassword, startChangePassword } from "./password";
|
||||||
|
import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password changed login", async ({ user, page }) => {
|
||||||
|
const changedPw = "ChangedPw1!";
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
|
||||||
|
// wait for projection of token
|
||||||
|
await page.waitForTimeout(10000);
|
||||||
|
|
||||||
|
await startChangePassword(page, user.getUsername());
|
||||||
|
await changePassword(page, changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
|
||||||
|
await loginWithPassword(page, user.getUsername(), changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("password change not with desired complexity", async ({ user, page }) => {
|
||||||
|
const changedPw1 = "change";
|
||||||
|
const changedPw2 = "chang";
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await startChangePassword(page, user.getUsername());
|
||||||
|
await changePasswordScreen(page, changedPw1, changedPw2);
|
||||||
|
await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
|
||||||
|
});
|
@@ -0,0 +1,98 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { code, codeResend, otpFromSink } from "./code";
|
||||||
|
import { codeScreenExpect } from "./code-screen";
|
||||||
|
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } from "./login";
|
||||||
|
import { OtpType, PasswordUserWithOTP } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUserWithOTP({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
type: OtpType.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("DOESN'T WORK: username, password and email otp login, enter code manually", async ({ user, page }) => {
|
||||||
|
// Given email otp is enabled on the organization of the user
|
||||||
|
// Given the user has only email otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User enters the code into the ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
await loginWithPasswordAndEmailOTP(page, user.getUsername(), user.getPassword(), user.getUsername());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and email otp login, click link in email", async ({ page }) => {
|
||||||
|
base.skip();
|
||||||
|
// Given email otp is enabled on the organization of the user
|
||||||
|
// Given the user has only email otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User clicks link in the email
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("DOESN'T WORK: username, password and email otp login, resend code", async ({ user, page }) => {
|
||||||
|
// Given email otp is enabled on the organization of the user
|
||||||
|
// Given the user has only email otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User clicks resend code
|
||||||
|
// User receives a new email with a verification code
|
||||||
|
// User enters the new code in the ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await codeResend(page);
|
||||||
|
await otpFromSink(page, user.getUsername());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and email otp login, wrong code", async ({ user, page }) => {
|
||||||
|
// Given email otp is enabled on the organization of the user
|
||||||
|
// Given the user has only email otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User enters a wrong code
|
||||||
|
// Error message - "Invalid code" is shown
|
||||||
|
const c = "wrongcode";
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await code(page, c);
|
||||||
|
await codeScreenExpect(page, c);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and email otp login, multiple mfa options", async ({ page }) => {
|
||||||
|
base.skip();
|
||||||
|
// Given email otp and sms otp is enabled on the organization of the user
|
||||||
|
// Given the user has email and sms otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User clicks button to use sms otp as second factor
|
||||||
|
// User receives a sms with a verification code
|
||||||
|
// User enters code in ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { code } from "./code";
|
||||||
|
import { codeScreenExpect } from "./code-screen";
|
||||||
|
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } from "./login";
|
||||||
|
import { OtpType, PasswordUserWithOTP } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUserWithOTP({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number({ style: "international" }),
|
||||||
|
isPhoneVerified: true,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
type: OtpType.sms,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("DOESN'T WORK: username, password and sms otp login, enter code manually", async ({ user, page }) => {
|
||||||
|
// Given sms otp is enabled on the organization of the user
|
||||||
|
// Given the user has only sms otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives a sms with a verification code
|
||||||
|
// User enters the code into the ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("DOESN'T WORK: username, password and sms otp login, resend code", async ({ user, page }) => {
|
||||||
|
// Given sms otp is enabled on the organization of the user
|
||||||
|
// Given the user has only sms otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives a sms with a verification code
|
||||||
|
// User clicks resend code
|
||||||
|
// User receives a new sms with a verification code
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and sms otp login, wrong code", async ({ user, page }) => {
|
||||||
|
// Given sms otp is enabled on the organization of the user
|
||||||
|
// Given the user has only sms otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// User receives a sms with a verification code
|
||||||
|
// User enters a wrong code
|
||||||
|
// Error message - "Invalid code" is shown
|
||||||
|
const c = "wrongcode";
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await code(page, c);
|
||||||
|
await codeScreenExpect(page, c);
|
||||||
|
});
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect, loginWithPassword, startLogin } from "./login";
|
||||||
|
import { loginname } from "./loginname";
|
||||||
|
import { resetPassword, startResetPassword } from "./password";
|
||||||
|
import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password set login", async ({ user, page }) => {
|
||||||
|
const changedPw = "ChangedPw1!";
|
||||||
|
await startLogin(page);
|
||||||
|
await loginname(page, user.getUsername());
|
||||||
|
await resetPassword(page, user.getUsername(), changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
|
||||||
|
await loginWithPassword(page, user.getUsername(), changedPw);
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("password set not with desired complexity", async ({ user, page }) => {
|
||||||
|
const changedPw1 = "change";
|
||||||
|
const changedPw2 = "chang";
|
||||||
|
await startLogin(page);
|
||||||
|
await loginname(page, user.getUsername());
|
||||||
|
await startResetPassword(page);
|
||||||
|
await resetPasswordScreen(page, user.getUsername(), changedPw1, changedPw2);
|
||||||
|
await resetPasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
|
||||||
|
});
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { code } from "./code";
|
||||||
|
import { codeScreenExpect } from "./code-screen";
|
||||||
|
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "./login";
|
||||||
|
import { PasswordUserWithTOTP } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUserWithTOTP({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number({ style: "international" }),
|
||||||
|
isPhoneVerified: true,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and totp login", async ({ user, page }) => {
|
||||||
|
// Given totp is enabled on the organization of the user
|
||||||
|
// Given the user has only totp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// Screen for entering the code is shown directly
|
||||||
|
// User enters the code into the ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and totp otp login, wrong code", async ({ user, page }) => {
|
||||||
|
// Given totp is enabled on the organization of the user
|
||||||
|
// Given the user has only totp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// Screen for entering the code is shown directly
|
||||||
|
// User enters a wrond code
|
||||||
|
// Error message - "Invalid code" is shown
|
||||||
|
const c = "wrongcode";
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await code(page, c);
|
||||||
|
await codeScreenExpect(page, c);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and totp login, multiple mfa options", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given totp and email otp is enabled on the organization of the user
|
||||||
|
// Given the user has totp and email otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// Screen for entering the code is shown directly
|
||||||
|
// Button to switch to email otp is shown
|
||||||
|
// User clicks button to use email otp instead
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User enters code in ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("username, password and u2f login", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given u2f is enabled on the organization of the user
|
||||||
|
// Given the user has only u2f configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// Popup for u2f is directly opened
|
||||||
|
// User verifies u2f
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username, password and u2f login, multiple mfa options", async ({ page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given u2f and semailms otp is enabled on the organization of the user
|
||||||
|
// Given the user has u2f and email otp configured as second factor
|
||||||
|
// User enters username
|
||||||
|
// User enters password
|
||||||
|
// Popup for u2f is directly opened
|
||||||
|
// User aborts u2f verification
|
||||||
|
// User clicks button to use email otp as second factor
|
||||||
|
// User receives an email with a verification code
|
||||||
|
// User enters code in ui
|
||||||
|
// User is redirected to the app (default redirect url)
|
||||||
|
});
|
157
login/apps/login-test-acceptance/tests/username-password.spec.ts
Normal file
157
login/apps/login-test-acceptance/tests/username-password.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import path from "path";
|
||||||
|
import { loginScreenExpect, loginWithPassword, startLogin } from "./login";
|
||||||
|
import { loginname } from "./loginname";
|
||||||
|
import { loginnameScreenExpect } from "./loginname-screen";
|
||||||
|
import { password } from "./password";
|
||||||
|
import { passwordScreenExpect } from "./password-screen";
|
||||||
|
import { PasswordUser } from "./user";
|
||||||
|
|
||||||
|
// Read from ".env" file.
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
const test = base.extend<{ user: PasswordUser }>({
|
||||||
|
user: async ({ page }, use) => {
|
||||||
|
const user = new PasswordUser({
|
||||||
|
email: faker.internet.email(),
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: faker.person.firstName(),
|
||||||
|
lastName: faker.person.lastName(),
|
||||||
|
organization: "",
|
||||||
|
phone: faker.phone.number(),
|
||||||
|
isPhoneVerified: false,
|
||||||
|
password: "Password1!",
|
||||||
|
passwordChangeRequired: false,
|
||||||
|
});
|
||||||
|
await user.ensure(page);
|
||||||
|
await use(user);
|
||||||
|
await user.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login", async ({ user, page }) => {
|
||||||
|
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||||
|
await loginScreenExpect(page, user.getFullName());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, unknown username", async ({ page }) => {
|
||||||
|
const username = "unknown";
|
||||||
|
await startLogin(page);
|
||||||
|
await loginname(page, username);
|
||||||
|
await loginnameScreenExpect(page, username);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, wrong password", async ({ user, page }) => {
|
||||||
|
await startLogin(page);
|
||||||
|
await loginname(page, user.getUsername());
|
||||||
|
await password(page, "wrong");
|
||||||
|
await passwordScreenExpect(page, "wrong");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, wrong username, ignore unknown usernames", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user doesn't exist but ignore unknown usernames setting is set to true
|
||||||
|
// Given username password login is enabled on the users organization
|
||||||
|
// enter login name
|
||||||
|
// enter password
|
||||||
|
// redirect to loginname page --> error message username or password wrong
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, initial password change", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user is created and has changePassword set to true
|
||||||
|
// Given username password login is enabled on the users organization
|
||||||
|
// enter login name
|
||||||
|
// enter password
|
||||||
|
// create new password
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, reset password hidden", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given the organization has enabled "Password reset hidden" in the login policy
|
||||||
|
// Given username password login is enabled on the users organization
|
||||||
|
// enter login name
|
||||||
|
// password reset link should not be shown on password screen
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, reset password - enter code manually", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user has forgotten password and clicks the forgot password button
|
||||||
|
// Given username password login is enabled on the users organization
|
||||||
|
// enter login name
|
||||||
|
// click password forgotten
|
||||||
|
// enter code from email
|
||||||
|
// user is redirected to app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, reset password - click link", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user has forgotten password and clicks the forgot password button, and then the link in the email
|
||||||
|
// Given username password login is enabled on the users organization
|
||||||
|
// enter login name
|
||||||
|
// click password forgotten
|
||||||
|
// click link in email
|
||||||
|
// set new password
|
||||||
|
// redirect to app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("username and password login, reset password, resend code", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user has forgotten password and clicks the forgot password button and then resend code
|
||||||
|
// Given username password login is enabled on the users organization
|
||||||
|
// enter login name
|
||||||
|
// click password forgotten
|
||||||
|
// click resend code
|
||||||
|
// enter code from second email
|
||||||
|
// user is redirected to app (default redirect url)
|
||||||
|
});
|
||||||
|
|
||||||
|
test("email login enabled", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// Given no other user with the same email address exists
|
||||||
|
// enter email address "test@zitadel.com " in login screen
|
||||||
|
// user will get to password screen
|
||||||
|
});
|
||||||
|
|
||||||
|
test("email login disabled", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// Given no other user with the same email address exists
|
||||||
|
// enter email address "test@zitadel.com" in login screen
|
||||||
|
// user will see error message "user not found"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("email login enabled - multiple users", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// enter email address "test@zitadel.com" in login screen
|
||||||
|
// user will see error message "user not found"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("phone login enabled", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// Given no other user with the same phon number exists
|
||||||
|
// enter phone number "0711111111" in login screen
|
||||||
|
// user will get to password screen
|
||||||
|
});
|
||||||
|
|
||||||
|
test("phone login disabled", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// Given no other user with the same phone number exists
|
||||||
|
// enter phone number "0711111111" in login screen
|
||||||
|
// user will see error message "user not found"
|
||||||
|
});
|
||||||
|
|
||||||
|
test("phone login enabled - multiple users", async ({ user, page }) => {
|
||||||
|
test.skip();
|
||||||
|
// Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists
|
||||||
|
// enter phone number "0711111111" in login screen
|
||||||
|
// user will see error message "user not found"
|
||||||
|
});
|
6
login/apps/login-test-acceptance/tests/welcome.ts
Normal file
6
login/apps/login-test-acceptance/tests/welcome.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("login is accessible", async ({ page }) => {
|
||||||
|
await page.goto("./");
|
||||||
|
await page.getByRole("heading", { name: "Welcome back!" }).isVisible();
|
||||||
|
});
|
190
login/apps/login-test-acceptance/tests/zitadel.ts
Normal file
190
login/apps/login-test-acceptance/tests/zitadel.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { Authenticator } from "@otplib/core";
|
||||||
|
import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
|
||||||
|
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin
|
||||||
|
import axios from "axios";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { request } from "gaxios";
|
||||||
|
import path from "path";
|
||||||
|
import { OtpType, userProps } from "./user";
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||||
|
|
||||||
|
export async function addUser(props: userProps) {
|
||||||
|
const body = {
|
||||||
|
username: props.email,
|
||||||
|
organization: {
|
||||||
|
orgId: props.organization,
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
givenName: props.firstName,
|
||||||
|
familyName: props.lastName,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
email: props.email,
|
||||||
|
isVerified: true,
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
phone: props.phone,
|
||||||
|
isVerified: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
password: props.password,
|
||||||
|
changeRequired: props.passwordChangeRequired ?? false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (!props.isEmailVerified) {
|
||||||
|
delete body.email.isVerified;
|
||||||
|
}
|
||||||
|
if (!props.isPhoneVerified) {
|
||||||
|
delete body.phone.isVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeUserByUsername(username: string) {
|
||||||
|
const resp = await getUserByUsername(username);
|
||||||
|
if (!resp || !resp.result || !resp.result[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await removeUser(resp.result[0].userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeUser(id: string) {
|
||||||
|
await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCall(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 400 && response.status !== 404) {
|
||||||
|
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error making request:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserByUsername(username: string): Promise<any> {
|
||||||
|
const listUsersBody = {
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
userNameQuery: {
|
||||||
|
userName: username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listCall(url: string, data: any): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, data, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error making request:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function activateOTP(userId: string, type: OtpType) {
|
||||||
|
let url = "otp_";
|
||||||
|
switch (type) {
|
||||||
|
case OtpType.sms:
|
||||||
|
url = url + "sms";
|
||||||
|
break;
|
||||||
|
case OtpType.email:
|
||||||
|
url = url + "email";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushCall(url: string, data: any) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, data, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error making request:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addTOTP(userId: string): Promise<string> {
|
||||||
|
const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {});
|
||||||
|
const code = totp(response.secret);
|
||||||
|
await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code });
|
||||||
|
return response.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totp(secret: string) {
|
||||||
|
const authenticator = new Authenticator({
|
||||||
|
createDigest,
|
||||||
|
createRandomBytes,
|
||||||
|
keyDecoder,
|
||||||
|
keyEncoder,
|
||||||
|
});
|
||||||
|
// google authenticator usage
|
||||||
|
const token = authenticator.generate(secret);
|
||||||
|
|
||||||
|
// check if token can be used
|
||||||
|
if (!authenticator.verify({ token: token, secret: secret })) {
|
||||||
|
const error = `Generated token could not be verified`;
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function eventualNewUser(id: string) {
|
||||||
|
return request({
|
||||||
|
url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
retryConfig: {
|
||||||
|
statusCodesToRetry: [[404, 404]],
|
||||||
|
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
|
||||||
|
totalTimeout: 10000, // 10 seconds
|
||||||
|
onRetryAttempt: (error) => {
|
||||||
|
console.warn(`Retrying to query new user ${id}: ${error.message}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
11
login/apps/login-test-acceptance/turbo.json
Normal file
11
login/apps/login-test-acceptance/turbo.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": ["//"],
|
||||||
|
"tasks": {
|
||||||
|
"test:acceptance:setup": {
|
||||||
|
"interactive": true,
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true,
|
||||||
|
"with": ["@zitadel/login#dev"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
login/apps/login-test-acceptance/zitadel.yaml
Normal file
83
login/apps/login-test-acceptance/zitadel.yaml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
ExternalDomain: 127.0.0.1.sslip.io
|
||||||
|
ExternalSecure: true
|
||||||
|
ExternalPort: 443
|
||||||
|
TLS.Enabled: false
|
||||||
|
|
||||||
|
FirstInstance:
|
||||||
|
PatPath: /pat/zitadel-admin-sa.pat
|
||||||
|
Org:
|
||||||
|
Human:
|
||||||
|
UserName: zitadel-admin
|
||||||
|
FirstName: ZITADEL
|
||||||
|
LastName: Admin
|
||||||
|
Password: Password1!
|
||||||
|
PasswordChangeRequired: false
|
||||||
|
PreferredLanguage: en
|
||||||
|
Machine:
|
||||||
|
Machine:
|
||||||
|
Username: zitadel-admin-sa
|
||||||
|
Name: Admin
|
||||||
|
Pat:
|
||||||
|
ExpirationDate: 2099-01-01T00:00:00Z
|
||||||
|
|
||||||
|
DefaultInstance:
|
||||||
|
LoginPolicy:
|
||||||
|
AllowUsernamePassword: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD
|
||||||
|
AllowRegister: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER
|
||||||
|
AllowExternalIDP: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP
|
||||||
|
ForceMFA: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA
|
||||||
|
HidePasswordReset: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET
|
||||||
|
IgnoreUnknownUsernames: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_IGNOREUNKNOWNUSERNAMES
|
||||||
|
AllowDomainDiscovery: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY
|
||||||
|
# 1 is allowed, 0 is not allowed
|
||||||
|
PasswordlessType: 1 # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE
|
||||||
|
# DefaultRedirectURL is empty by default because we use the Console UI
|
||||||
|
DefaultRedirectURI: # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_DEFAULTREDIRECTURI
|
||||||
|
# 240h = 10d
|
||||||
|
PasswordCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDCHECKLIFETIME
|
||||||
|
# 240h = 10d
|
||||||
|
ExternalLoginCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_EXTERNALLOGINCHECKLIFETIME
|
||||||
|
# 720h = 30d
|
||||||
|
MfaInitSkipLifetime: 0h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME
|
||||||
|
SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME
|
||||||
|
MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME
|
||||||
|
PrivacyPolicy:
|
||||||
|
TOSLink: "https://zitadel.com/docs/legal/terms-of-service"
|
||||||
|
PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy"
|
||||||
|
HelpLink: "https://zitadel.com/docs"
|
||||||
|
SupportEmail: "support@zitadel.com"
|
||||||
|
DocsLink: "https://zitadel.com/docs"
|
||||||
|
Features:
|
||||||
|
LoginV2:
|
||||||
|
Required: true
|
||||||
|
|
||||||
|
OIDC:
|
||||||
|
DefaultLoginURLV2: "/ui/v2/login/login?authRequest="
|
||||||
|
|
||||||
|
SAML:
|
||||||
|
DefaultLoginURLV2: "/ui/v2/login/login?authRequest="
|
||||||
|
|
||||||
|
Database:
|
||||||
|
EventPushConnRatio: 0.2 # 4
|
||||||
|
ProjectionSpoolerConnRatio: 0.3 # 6
|
||||||
|
postgres:
|
||||||
|
Host: db
|
||||||
|
Port: 5432
|
||||||
|
Database: zitadel
|
||||||
|
MaxOpenConns: 20
|
||||||
|
MaxIdleConns: 20
|
||||||
|
MaxConnLifetime: 1h
|
||||||
|
MaxConnIdleTime: 5m
|
||||||
|
User:
|
||||||
|
Username: zitadel
|
||||||
|
SSL:
|
||||||
|
Mode: disable
|
||||||
|
Admin:
|
||||||
|
Username: zitadel
|
||||||
|
SSL:
|
||||||
|
Mode: disable
|
||||||
|
|
||||||
|
Logstore:
|
||||||
|
Access:
|
||||||
|
Stdout:
|
||||||
|
Enabled: true
|
2
login/apps/login-test-integration/.gitignore
vendored
Normal file
2
login/apps/login-test-integration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
screenshots
|
||||||
|
videos
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user