From 4fa6699cb5a43416080e01d7f10ab37598cd310b Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:52:30 +0000 Subject: [PATCH] ci: enhance the security of gh workflows (#10564) * ci: enhance security of workflows * ci: fix docs git add of search folders * chore: remove jans-tent * docs: update docs with the removal of jans tent * ci: fix pip upgrade Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix ignore previously installed packages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up dep in build of assets Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up dep in build of assets Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix permission level for clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: skip deleting if the service doesn't have any packages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: load all pages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: load all pages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: load all pages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --------- Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --- .github/dependabot.yml | 4 - .github/workflows/build-docker-image.yml | 18 +- .github/workflows/build-docs.yml | 5 +- .github/workflows/build-packages.yml | 8 +- .github/workflows/build-test.yml | 67 +++-- .github/workflows/lint-flak8.yml | 17 +- .github/workflows/ops-docs.yml | 4 +- .github/workflows/ops-label-pr-issues.yml | 10 +- .github/workflows/release.yaml | 7 +- .github/workflows/sanitary-github-cache.yml | 3 +- .github/workflows/sanitary-workflow-runs.yml | 2 + .github/workflows/scan-sonar.yml | 7 +- .github/workflows/test-cedarling.yml | 4 +- .github/workflows/test-jans-pycloudlib.yml | 2 +- README.md | 1 - demos/README.md | 8 +- demos/jans-tent/.flaskenv | 2 - demos/jans-tent/.gitignore | 146 --------- demos/jans-tent/LICENSE | 201 ------------- demos/jans-tent/README.md | 144 --------- demos/jans-tent/behave.ini | 3 - demos/jans-tent/clientapp/__init__.py | 251 ---------------- demos/jans-tent/clientapp/config.py | 36 --- demos/jans-tent/clientapp/helpers/__init__.py | 0 .../clientapp/helpers/cgf_checker.py | 17 -- .../clientapp/helpers/client_handler.py | 117 -------- .../clientapp/helpers/custom_msg_factory.py | 59 ---- demos/jans-tent/clientapp/templates/home.html | 21 -- demos/jans-tent/clientapp/utils/__init__.py | 0 .../clientapp/utils/dcr_from_config.py | 41 --- demos/jans-tent/clientapp/utils/logger.py | 16 - .../docs/images/authorize_code_flow.png | Bin 53305 -> 0 bytes demos/jans-tent/main.py | 6 - demos/jans-tent/register_new_client.py | 12 - demos/jans-tent/requirements.txt | 119 -------- .../tests/behaver/features/environment.py | 31 -- .../tests/behaver/features/oidc_auth.feature | 40 --- .../features/passport_social_auth.feature | 26 -- .../tests/behaver/features/steps/allow.py | 116 -------- .../tests/unit_integration/helper.py | 189 ------------ .../test_callback_endpoint.py | 62 ---- .../unit_integration/test_cfg_checker.py | 0 .../test_client_register_endpoint.py | 145 --------- .../tests/unit_integration/test_config.py | 19 -- .../test_configuration_endpoint.py | 107 ------- .../unit_integration/test_dcr_from_config.py | 76 ----- .../test_dynamic_client_registration.py | 277 ------------------ .../unit_integration/test_flask_factory.py | 93 ------ .../test_gluu_preselected_provider.py | 46 --- .../unit_integration/test_logout_endpoint.py | 65 ---- .../test_protected_content_endpoint.py | 68 ----- .../agama/quick-start-using-agama-lab.md | 2 +- 52 files changed, 103 insertions(+), 2617 deletions(-) delete mode 100644 demos/jans-tent/.flaskenv delete mode 100644 demos/jans-tent/.gitignore delete mode 100644 demos/jans-tent/LICENSE delete mode 100644 demos/jans-tent/README.md delete mode 100644 demos/jans-tent/behave.ini delete mode 100644 demos/jans-tent/clientapp/__init__.py delete mode 100644 demos/jans-tent/clientapp/config.py delete mode 100644 demos/jans-tent/clientapp/helpers/__init__.py delete mode 100644 demos/jans-tent/clientapp/helpers/cgf_checker.py delete mode 100644 demos/jans-tent/clientapp/helpers/client_handler.py delete mode 100644 demos/jans-tent/clientapp/helpers/custom_msg_factory.py delete mode 100644 demos/jans-tent/clientapp/templates/home.html delete mode 100644 demos/jans-tent/clientapp/utils/__init__.py delete mode 100644 demos/jans-tent/clientapp/utils/dcr_from_config.py delete mode 100644 demos/jans-tent/clientapp/utils/logger.py delete mode 100644 demos/jans-tent/docs/images/authorize_code_flow.png delete mode 100644 demos/jans-tent/main.py delete mode 100644 demos/jans-tent/register_new_client.py delete mode 100644 demos/jans-tent/requirements.txt delete mode 100644 demos/jans-tent/tests/behaver/features/environment.py delete mode 100644 demos/jans-tent/tests/behaver/features/oidc_auth.feature delete mode 100644 demos/jans-tent/tests/behaver/features/passport_social_auth.feature delete mode 100644 demos/jans-tent/tests/behaver/features/steps/allow.py delete mode 100644 demos/jans-tent/tests/unit_integration/helper.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_callback_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_cfg_checker.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_config.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_dcr_from_config.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_flask_factory.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_logout_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33fe6fac22..97049410915 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,10 +26,6 @@ updates: schedule: interval: daily - - package-ecosystem: pip - directory: /demos/jans-tent - schedule: - interval: daily - package-ecosystem: docker directory: /docker-jans-all-in-one diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index d398da52eca..6a0dfef7cb1 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -57,7 +57,7 @@ jobs: egress-policy: audit - name: Install Cosign - uses: sigstore/cosign-installer@v3.5.0 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -91,9 +91,9 @@ jobs: if: steps.build_docker_image.outputs.build || github.event_name == 'tags' run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update #- uses: actions/delete-package-versions@v5 @@ -165,19 +165,19 @@ jobs: fi # UPDATE BUILD DATES INSIDE THE DOCKERFILE BEFORE BUILDING THE DEV IMAGES TRIGGERED BY JENKINS - - name: Setup Python 3.7 + - name: Setup Python 3.10 if: github.event_name == 'workflow_dispatch' && ${{ matrix.docker-images }} != 'loadtesting-jmeter' uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Install Python dependencies if: github.event_name == 'workflow_dispatch' && ${{ matrix.docker-images }} != 'loadtesting-jmeter' run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update sudo apt-get install jq diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b1b771aa930..28e9ce016b9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -209,7 +209,10 @@ jobs: # END move generated chart from a previous step # copy search from nightly to all other versions. This is to ensure that the search index is available for all versions - for folder in v*/; do cp -r nightly/search "$folder"; done + for folder in v*/; do + cp -r nightly/search "$folder" + git add $folder/search && git update-index --refresh + done # END copy search from nightly to all other versions echo "Replacing release number markers with actual release number" diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 6d5687ec882..258caa392d2 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -5,6 +5,8 @@ on: tags: - 'v**' - 'nightly' +permissions: + contents: read jobs: publish_binary_packages: if: github.repository == 'JanssenProject/jans' @@ -196,7 +198,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y python3 build-essential ca-certificates dbus systemd iproute2 gpg python3-pip python3-dev libpq-dev gcc - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" pip3 install shiv wheel setuptools echo "Building jans-linux-setup package" sudo chown -R runner:docker /home/runner/work/jans/jans @@ -356,8 +358,8 @@ jobs: git_user_signingkey: true git_commit_gpgsign: true - - uses: actions/setup-python@v5 - - uses: PyO3/maturin-action@v1 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + - uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab # v1.45.0 with: working-directory: ${{ github.workspace }}/jans-cedarling/bindings/cedarling_python command: build diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0142d4bc5b4..845e8702a16 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -40,35 +40,50 @@ on: concurrency: group: run-once cancel-in-progress: false +permissions: + contents: read jobs: cleanup: - if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' runs-on: ubuntu-20.04 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + packages: write steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit - name: Get version ID for 0.0.0-nightly - if: github.event_name == 'push' && github.ref == 'refs/heads/main' id: get_version_id run: | - services=$(gh api -H "Accept: application/vnd.github+json" \ - /orgs/JanssenProject/packages?package_type=maven \ - | jq -r '.[].name') - for service in "${services}"; do - version_id=$(gh api -H "Accept: application/vnd.github+json" \ - /orgs/JanssenProject/packages/maven/io.jans.${service}/versions \ - | jq -r '.[] | select(.name == "0.0.0-nightly") | .id') - echo "version_id=$version_id" >> $GITHUB_ENV - gh api --method DELETE \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /orgs/JanssenProject/packages/maven/io.jans."${service}"/versions/"${version_id}" + page=1 + services="" + while true; do + response=$(gh api -H "Accept: application/vnd.github+json" \ + /orgs/JanssenProject/packages?package_type=maven\&per_page=100\&page=$page) + names=$(echo "$response" | jq -r '.[].name') + if [ -z "$names" ]; then + break + fi + services="$services $names" + page=$((page + 1)) done - + + services=$(echo "$services" | tr '\n' ' ' | sed 's/ *$//') + echo "Services: $services" + for service in $services; do + echo "Checking $service" + version_id=$(gh api -H "Accept: application/vnd.github+json" \ + /orgs/JanssenProject/packages/maven/"${service}"/versions \ + | jq -r '.[] | select(.name == "0.0.0-nightly") | .id') + echo "version_id=$version_id" >> $GITHUB_ENV + gh api --method DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /orgs/JanssenProject/packages/maven/"${service}"/versions/"${version_id}" || echo "Failed to delete $service" + done prep-matrix: needs: cleanup @@ -126,18 +141,18 @@ jobs: with: egress-policy: audit - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.inputs.branch }} - name: Set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' - name: Set up Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' @@ -159,7 +174,7 @@ jobs: - name: Archive results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: build-results path: ${{ matrix.service }}/target @@ -170,7 +185,9 @@ jobs: run-tests: if: github.event_name == 'push' || github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.event.inputs.project == 'jans-bom, jans-orm, jans-core, jans-lock/lock-server, agama, jans-auth-server, jans-link, jans-fido2, jans-scim, jans-keycloak-link, jans-config-api, jans-keycloak-integration, jans-casa') - permissions: write-all + permissions: + contents: read + packages: write needs: cleanup runs-on: ubuntu-20.04 env: @@ -198,18 +215,18 @@ jobs: with: egress-policy: audit - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.inputs.branch }} - name: Set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' - name: Set up Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' @@ -276,13 +293,13 @@ jobs: ls /tmp/reports/ - name: Upload Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: ${{ matrix.persistence }}-test-results path: /tmp/reports - name: Publish Test Report ${{ matrix.persistence }} - uses: starburstdata/action-testng-report@v1 + uses: starburstdata/action-testng-report@f245422953fb97ec5075d07782a1b596124b7cc4 # v1.0.5 with: report_paths: /tmp/reports/${{ matrix.persistence }}*.xml github_token: ${{ github.token }} diff --git a/.github/workflows/lint-flak8.yml b/.github/workflows/lint-flak8.yml index 63dadc76b64..e8eff713621 100644 --- a/.github/workflows/lint-flak8.yml +++ b/.github/workflows/lint-flak8.yml @@ -4,14 +4,16 @@ on: branches: - main paths: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - - 'demos/jans-tent/**' + - 'jans-pycloudlib/**' + - 'jans-cli-tui/**' + - 'jans-linux-setup/**' pull_request: branches: - main paths: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - - 'demos/jans-tent/**' + - 'jans-pycloudlib/**' + - 'jans-cli-tui/**' + - 'jans-linux-setup/**' permissions: contents: read @@ -23,8 +25,11 @@ jobs: #max-parallel: 1 fail-fast: false matrix: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - python-projects: ["demos/jans-tent"] + python-projects: [ + "jans-pycloudlib", + "jans-cli-tui", + "jans-linux-setup" + ] steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 diff --git a/.github/workflows/ops-docs.yml b/.github/workflows/ops-docs.yml index 07c61013b3f..34311ad3e04 100644 --- a/.github/workflows/ops-docs.yml +++ b/.github/workflows/ops-docs.yml @@ -71,10 +71,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Python 3.7 + - name: Set up Python 3.10 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Auto-merge inhouse doc prs run: | diff --git a/.github/workflows/ops-label-pr-issues.yml b/.github/workflows/ops-label-pr-issues.yml index 73528021229..bf0b1cd5f83 100644 --- a/.github/workflows/ops-label-pr-issues.yml +++ b/.github/workflows/ops-label-pr-issues.yml @@ -31,17 +31,17 @@ jobs: - name: check out code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Setup Python 3.7 + - name: Set up Python 3.10 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Install dependencies run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update sudo apt-get install jq curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ca0b9fcd917..d7df2a159cb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,12 +10,17 @@ jobs: strategy: fail-fast: false steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f # v4.1.3 id: release-please with: release-type: simple diff --git a/.github/workflows/sanitary-github-cache.yml b/.github/workflows/sanitary-github-cache.yml index b2bfb70f57d..e1dd3fa9676 100644 --- a/.github/workflows/sanitary-github-cache.yml +++ b/.github/workflows/sanitary-github-cache.yml @@ -4,7 +4,8 @@ on: types: - closed workflow_dispatch: - +permissions: + contents: read jobs: cleanup: runs-on: ubuntu-latest diff --git a/.github/workflows/sanitary-workflow-runs.yml b/.github/workflows/sanitary-workflow-runs.yml index fd3137becc7..c8115cc62a8 100644 --- a/.github/workflows/sanitary-workflow-runs.yml +++ b/.github/workflows/sanitary-workflow-runs.yml @@ -3,6 +3,8 @@ on: schedule: - cron: '0 0 */2 * *' workflow_dispatch: +permissions: + contents: read jobs: del_runs: runs-on: ubuntu-latest diff --git a/.github/workflows/scan-sonar.yml b/.github/workflows/scan-sonar.yml index 66284080304..69cac4cfc36 100644 --- a/.github/workflows/scan-sonar.yml +++ b/.github/workflows/scan-sonar.yml @@ -55,7 +55,8 @@ on: - '!**.txt' workflow_dispatch: - +permissions: + contents: read jobs: sonar-scan: name: sonar scan @@ -82,7 +83,9 @@ jobs: jans-linux-setup jans-cli-tui jans-pycloudlib - + permissions: + contents: read + pull-requests: read steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 diff --git a/.github/workflows/test-cedarling.yml b/.github/workflows/test-cedarling.yml index 6647eba00c0..896caa6c449 100644 --- a/.github/workflows/test-cedarling.yml +++ b/.github/workflows/test-cedarling.yml @@ -19,7 +19,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable - name: Run Tests run: | cd ./jans-cedarling @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" python3 -m pip install tox - name: Test with pytest run: | diff --git a/.github/workflows/test-jans-pycloudlib.yml b/.github/workflows/test-jans-pycloudlib.yml index 3603b64f320..b673adb249f 100644 --- a/.github/workflows/test-jans-pycloudlib.yml +++ b/.github/workflows/test-jans-pycloudlib.yml @@ -41,7 +41,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" python3 -m pip install tox - name: Test with pytest run: | diff --git a/README.md b/README.md index 44b01972fad..5aca229dbed 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ commercial distribution of Janssen Project Components called | **[Jans Lock](jans-lock)** | An enterprise authorization solution featuring the Cedarling, a stateless PDP and the Lock Server which centralizes audit logs and configuration. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Tarp](demos/jans-tarp)** | An OpenID Connect RP test website that runs as a browser plugin in Chrome or Firefox. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Chip](demos/jans-chip)** | Sample iOS and Android mobile applications that implement the full OAuth and FIDO security stack for app integrity, client constrained access tokens, and user presence. | ![Demo](https://img.shields.io/badge/Demo-%23368af7) | -| **[Jans Tent](demos/jans-tent)** | A test Relying Party ("RP") built using Python and Flask. Enables you to send different requests by quickly modifying just one configuration file. | ![Demo](https://img.shields.io/badge/Demo-%23368af7) | ## Installation diff --git a/demos/README.md b/demos/README.md index d16501ccf25..66b93674826 100644 --- a/demos/README.md +++ b/demos/README.md @@ -4,6 +4,10 @@ This folder holds different demos for different applications with janssen author ## [Benchmarking](benchmarking) Holds a docker load test image packaging for Janssen. This image can load test users to a janssen environment and can execute jmeter tests. -## [Jans-tent](jans-tent) -Reliable OpenID client to be used in auth testing. +## [Janssen Chip](jans-chip) +- A first party android mobile application that leverages dynamic client registration (DCR), DPoP access tokens. +- Passkey authentication + +## [Janssen Tarp](jans-tarp) +A Relying Party tool in form of a Browser Extension for convenient testing of authentication flows on a browser. diff --git a/demos/jans-tent/.flaskenv b/demos/jans-tent/.flaskenv deleted file mode 100644 index bc1b2cf6e71..00000000000 --- a/demos/jans-tent/.flaskenv +++ /dev/null @@ -1,2 +0,0 @@ -#.flaskenv -FLASK_APP=clientapp diff --git a/demos/jans-tent/.gitignore b/demos/jans-tent/.gitignore deleted file mode 100644 index 6b3dc1fcd19..00000000000 --- a/demos/jans-tent/.gitignore +++ /dev/null @@ -1,146 +0,0 @@ -#jans-tent-specific -client_info.json -*.log.* - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -.vscode/ -.scannerwork - diff --git a/demos/jans-tent/LICENSE b/demos/jans-tent/LICENSE deleted file mode 100644 index 6912a5f93c9..00000000000 --- a/demos/jans-tent/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 Christian Eland - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/demos/jans-tent/README.md b/demos/jans-tent/README.md deleted file mode 100644 index 3f6c6c1ff87..00000000000 --- a/demos/jans-tent/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# Jans Tent - -To test an OpenID Provider ("OP"), you need a test Relying Party ("RP"). Jans -Tent is easy to configure RP which enables you to send different requests by -quickly modifying one file (`config.py`). It's a Python Flask application, -so it's easy to hack for other testing requirements. - -By default, it uses `localhost` as the `redirect_uri`, so if you run it on your -laptop, all you need to do is specify the OP hostname to run it. Tent uses -dynamic client registration to obtain client credentials. But you can also use -an existing client_id if you like. - -## Installation - -**Important**: Ensure you have `Python >= 3.11` - -**Mac Users**: We recommend using [pyenv - simple python version management](https://github.com/pyenv/pyenv) instead of Os x native python. - -1. Navigate to the project root folder `jans/demos/jans-tent` -2. Create virtual environment -```bash -python3 -m venv venv -```` -3. Activate the virtual virtual environment -```bash -source venv/bin/activate -``` -4. Install dependencies -```bash -pip install -r requirements.txt -``` - -## Setup - -### 1. Edit configuration file `clientapp/config.py` according to your needs: - * Set `ISSUER`, replace `op_hostname` (required) - * Set any other desired configuration - -### 2. Generate test RP server self signed certs - -Generate `key.pem` and `cert.pem` at `jans-tent` project root folder (`jans/demos/jans-tent`). i.e: -```bash -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -``` - -### 3. Import your OP TLS certificate - -(remember to be inside your virtual environment) - -Supply the hostname of the ISSUER after the `=` - -```bash -export OP_HOSTNAME= -``` - -```bash -echo | openssl s_client -servername $OP_HOSTNAME -connect $OP_HOSTNAME:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > op_web_cert.cer -``` - -```bash -export CERT_PATH=$(python3 -m certifi) -``` - -```bash -export SSL_CERT_FILE=${CERT_PATH} -``` - -```bash -export REQUESTS_CA_BUNDLE=${CERT_PATH} && mv op_web_cert.cer $CERT_PATH -``` - -## Using the server - -### Start the server - -Please notice that your client will be automatically registered once the server -starts. If your client was already registered, when you start the server again, -it won't register. Remember to be inside your virtual environment! - -```bash -python main.py -``` - -### Login! - -Navigate your browser to `https://localhost:9090` and click the link to start. - -## Manual client configuration - -In case your OP doesn't support dynamic registration, manually configure your -client by creating a file caled `client_info.json` in the `jans-tent` folder -with the following claims: - -```json -{ - "op_metadata_url": "https://op_hostname/.well-known/openid-configuration", - "client_id": "e4f2c3a9-0797-4c6c-9268-35c5546fb3e9", - "client_secret": "a3e71cf1-b9b4-44c5-a9e6-4c7b5c660a5d" -} -``` - -## Updating Tent to use a different OP - -If you want to test a different OP, do the following: - -1. Remove `op_web_cert` from the tent folder, and follow the procedure above -to download and install a new OP TLS certificate -2. Remove `client_info.json` from the tent folder -3. Update the value of `ISSUER` in `./clientapp/config.py` -4. Run `./register_new_client.py` - -## Other Tent endpoints - -### Auto-register endpoint - -Sending a `POST` request to Jans Tent `/register` endpoint containing a `JSON` -with the OP/AS url and client url, like this: - -```json -{ - "op_url": "https://OP_HOSTNAME", - "client_url": "https://localhost:9090", - "additional_params": { - "scope": "openid mail profile" - } -} -``` -Please notice that `additional_params` is not required by endpoint. - -The response will return the registered client id and client secret - -### Auto-config endpoint - -Sending a `POST` request to the Tent `/configuration` endpoint, containing the -client id, client secret, and metadata endpoint will fetch data from OP metadata -url and override the `config.py` settings during runtime. - -```json -{ - "client_id": "e4f2c3a9-0797-4c6c-9268-35c5546fb3e9", - "client_secret": "5c9e4775-0f1d-4a56-87c9-a629e1f88b9b", - "op_metadata_url": "https://OP_HOSTNAME/.well-known/openid-configuration" -} -``` diff --git a/demos/jans-tent/behave.ini b/demos/jans-tent/behave.ini deleted file mode 100644 index cbb1bc67a71..00000000000 --- a/demos/jans-tent/behave.ini +++ /dev/null @@ -1,3 +0,0 @@ -[behave] -stderr_capture=False -stdout_capture=False diff --git a/demos/jans-tent/clientapp/__init__.py b/demos/jans-tent/clientapp/__init__.py deleted file mode 100644 index a7429e815a5..00000000000 --- a/demos/jans-tent/clientapp/__init__.py +++ /dev/null @@ -1,251 +0,0 @@ -''' -Project: Test Auth Client -Author: Christian Hawk - - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import base64 -import urllib -import json -import os -from urllib.parse import urlparse -from authlib.integrations.flask_client import OAuth -from flask import (Flask, jsonify, redirect, render_template, request, session, - url_for) -from . import config as cfg -from .helpers.client_handler import ClientHandler -from .helpers.cgf_checker import register_client_if_no_client_info -from .utils.logger import setup_logger - -setup_logger() - -oauth = OAuth() - - -def add_config_from_json(): - with open('client_info.json', 'r') as openfile: - client_info = json.load(openfile) - cfg.SERVER_META_URL = client_info['op_metadata_url'] - cfg.CLIENT_ID = client_info['client_id'] - cfg.CLIENT_SECRET = client_info['client_secret'] - cfg.END_SESSION_ENDPOINT = client_info['end_session_endpoint'] # separate later - - -def get_preselected_provider(): - provider_id_string = cfg.PRE_SELECTED_PROVIDER_ID - provider_object = '{ "provider" : "%s" }' % provider_id_string - provider_object_bytes = provider_object.encode() - base64url_bytes = base64.urlsafe_b64encode(provider_object_bytes) - base64url_value = base64url_bytes.decode() - # if base64url_value.endswith('='): - # base64url_value_unpad = base64url_value.replace('=', '') - # return base64url_value_unpad - return base64url_value - - -def get_provider_host(): - provider_host_string = cfg.PROVIDER_HOST_STRING - provider_object = '{ "providerHost" : "%s" }' % provider_host_string - provider_object_bytes = provider_object.encode() - base64url_bytes = base64.urlsafe_b64encode(provider_object_bytes) - base64url_value = base64url_bytes.decode() - # if base64url_value.endswith('='): - # base64url_value_unpad = base64url_value.replace('=', '') - # return base64url_value_unpad - return base64url_value - - -def ssl_verify(ssl_verify=cfg.SSL_VERIFY): - if ssl_verify is False: - os.environ['CURL_CA_BUNDLE'] = "" - - -class BaseClientErrors(Exception): - status_code = 500 - - -def create_app(): - register_client_if_no_client_info() - add_config_from_json() - ssl_verify() - - app = Flask(__name__) - - app.secret_key = b'fasfafpj3rasdaasfglaksdgags331s' - app.config['OP_CLIENT_ID'] = cfg.CLIENT_ID - app.config['OP_CLIENT_SECRET'] = cfg.CLIENT_SECRET - oauth.init_app(app) - oauth.register( - 'op', - server_metadata_url=cfg.SERVER_META_URL, - client_kwargs={ - 'scope': cfg.SCOPE - }, - token_endpoint_auth_method=cfg.SERVER_TOKEN_AUTH_METHOD - ) - - @app.route('/') - def index(): - user = session.get('user') - id_token = session.get('id_token') - return render_template("home.html", user=user, id_token=id_token) - - @app.route('/logout') - def logout(): - app.logger.info('Called /logout') - if 'id_token' in session.keys(): - app.logger.info('Cleaning session credentials') - token_hint = session.get('id_token') - session.pop('id_token') - session.pop('user') - parsed_redirect_uri = urllib.parse.urlparse(cfg.REDIRECT_URIS[0]) - post_logout_redirect_uri = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - return redirect( - '%s?post_logout_redirect_uri=%s&token_hint=%s' % ( - cfg.END_SESSION_ENDPOINT, post_logout_redirect_uri, token_hint - ) - ) - - app.logger.info('Not authorized to logout, redirecting to index') - return redirect(url_for('index')) - - @app.route('/register', methods=['POST']) - def register(): - app.logger.info('/register called') - content = request.json - app.logger.debug('data = %s' % content) - status = 0 - data = '' - if content is None: - status = 400 - # message = 'No json data posted' - elif 'op_url' and 'redirect_uris' not in content: - status = 400 - # message = 'Not needed keys found in json' - else: - app.logger.info('Trying to register client %s on %s' % - (content['redirect_uris'], content['op_url'])) - op_url = content['op_url'] - redirect_uris = content['redirect_uris'] - - op_parsed_url = urlparse(op_url) - client_parsed_redirect_uri = urlparse(redirect_uris[0]) - - if op_parsed_url.scheme != 'https' or client_parsed_redirect_uri.scheme != 'https': - status = 400 - - elif ((( - op_parsed_url.path != '' or op_parsed_url.query != '') or client_parsed_redirect_uri.path == '') or client_parsed_redirect_uri.query != ''): - status = 400 - - else: - additional_metadata = {} - if 'additional_params' in content.keys(): - additional_metadata = content['additional_params'] - client_handler = ClientHandler( - content['op_url'], content['redirect_uris'], additional_metadata - ) - data = client_handler.get_client_dict() - status = 200 - return jsonify(data), status - - @app.route('/protected-content', methods=['GET']) - def protected_content(): - app.logger.debug('/protected-content - cookies = %s' % request.cookies) - app.logger.debug('/protected-content - session = %s' % session) - if 'user' in session: - return session['user'] - - return redirect(url_for('login')) - - @app.route('/login') - def login(): - app.logger.info('/login requested') - redirect_uri = cfg.REDIRECT_URIS[0] - app.logger.debug('/login redirect_uri = %s' % redirect_uri) - # response = oauth.op.authorize_redirect() - query_args = { - 'redirect_uri': redirect_uri, - } - - if cfg.ACR_VALUES is not None: - query_args['acr_values'] = cfg.ACR_VALUES - - # used for inbound-saml, uncomment and set config.py to use it - # if cfg.PRE_SELECTED_PROVIDER is True: - # query_args[ - # 'preselectedExternalProvider'] = get_preselected_provider() - - # used for gluu-passport, , uncomment and set config.py to use it - # if cfg.PROVIDER_HOST_STRING is not None: - # query_args["providerHost"] = get_provider_host() - - if cfg.ADDITIONAL_PARAMS is not None: - query_args |= cfg.ADDITIONAL_PARAMS - - response = oauth.op.authorize_redirect(**query_args) - - app.logger.debug('/login authorize_redirect(redirect_uri) url = %s' % - (response.location)) - - return response - - @app.route('/oidc_callback') - @app.route('/callback') - def callback(): - try: - if not request.args['code']: - return {}, 400 - - app.logger.info('/callback - received %s - %s' % - (request.method, request.query_string)) - token = oauth.op.authorize_access_token() - app.logger.debug('/callback - token = %s' % token) - user = oauth.op.userinfo() - app.logger.debug('/callback - user = %s' % user) - session['user'] = user - session['id_token'] = token['userinfo'] - app.logger.debug('/callback - cookies = %s' % request.cookies) - app.logger.debug('/callback - session = %s' % session) - - return redirect('/') - - except Exception as error: - app.logger.error(str(error)) - return {'error': str(error)}, 400 - - @app.route("/configuration", methods=["POST"]) - def configuration(): - # Receives client configuration via API - app.logger.info('/configuration called') - content = request.json - app.logger.debug("content = %s" % content) - if content is not None: - if 'provider_id' in content: - cfg.PRE_SELECTED_PROVIDER_ID = content['provider_id'] - cfg.PRE_SELECTED_PROVIDER = True - app.logger.debug('/configuration: provider_id = %s' % - content['provider_id']) - - return jsonify({"provider_id": content['provider_id']}), 200 - - if "client_id" in content and "client_secret" in content: - # Setup client_id and client_secret - oauth.op.client_id = content['client_id'] - oauth.op.client_secret = content['client_secret'] - return {}, 200 - else: - return {}, 400 - - return app diff --git a/demos/jans-tent/clientapp/config.py b/demos/jans-tent/clientapp/config.py deleted file mode 100644 index 04bcb8df3b9..00000000000 --- a/demos/jans-tent/clientapp/config.py +++ /dev/null @@ -1,36 +0,0 @@ -# REQUIRED -# Replace op_hostname -ISSUER = 'https://op_hostname' - -# Tent redirect uri -REDIRECT_URIS = [ - 'https://localhost:9090/oidc_callback' -] - -# OPTIONAL: Use at your own risk - -# Token authentication method can be -# client_secret_basic -# client_secret_post -# none -SERVER_TOKEN_AUTH_METHOD = 'client_secret_post' - -# ACR VALUES -# Examples: -# ACR_VALUES = "agama" -# ACR_VALUES = 'simple_password_auth' -ACR_VALUES = None - -# ADDITIONAL PARAMS TO CALL AUTHORIZE ENDPOINT, WITHOUT BASE64 ENCODING. USE DICT {'param': 'value'} -# ADDITIONAL_PARAMS = {'paramOne': 'valueOne', 'paramTwo': 'valueTwo'} -ADDITIONAL_PARAMS = None - -# SYSTEM SETTINGS -# use with caution, unsecure requests, for development environments -SSL_VERIFY = False - -# SCOPES -# Only scope "openid" is required for a pairwise identifier from the OP. -# OP can provision additional optional scopes as needed. -# SCOPE = 'openid email profile' -SCOPE = 'openid' diff --git a/demos/jans-tent/clientapp/helpers/__init__.py b/demos/jans-tent/clientapp/helpers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/clientapp/helpers/cgf_checker.py b/demos/jans-tent/clientapp/helpers/cgf_checker.py deleted file mode 100644 index e5ade597adf..00000000000 --- a/demos/jans-tent/clientapp/helpers/cgf_checker.py +++ /dev/null @@ -1,17 +0,0 @@ -from os.path import exists -import logging -from clientapp.utils.dcr_from_config import register - -logger = logging.getLogger(__name__) - - -def configuration_exists() -> bool: - return exists('client_info.json') - - -def register_client_if_no_client_info() -> None: - if configuration_exists() : - logger.info('Found configuration file client_info.json, skipping auto-register') - else: - logger.info('Client configuration not found, trying to auto-register through DCR') - register() diff --git a/demos/jans-tent/clientapp/helpers/client_handler.py b/demos/jans-tent/clientapp/helpers/client_handler.py deleted file mode 100644 index 7e5f8f12e2a..00000000000 --- a/demos/jans-tent/clientapp/helpers/client_handler.py +++ /dev/null @@ -1,117 +0,0 @@ -''' -Project: Test Auth Client -Author: Christian Hawk - - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import logging -import json -from httplib2 import RelativeURIError -from typing import Optional, Dict, Any - -from oic.oauth2 import ASConfigurationResponse -from oic.oic import Client -from oic.utils.authn.client import CLIENT_AUTHN_METHOD -from .custom_msg_factory import CustomMessageFactory - - -logger = logging.getLogger(__name__) - - -class ClientHandler: - __redirect_uris = None - __client_id = None - __client_secret = None - __metadata_url = None - __op_url = None - __additional_metadata = None - __end_session_endpoint = None - op_data = None - - def __init__(self, op_url: str, redirect_uris: list[str], additional_metadata: dict): - """[initializes] - - :param op_url: [url from oidc provider starting with https] - :type op_url: str - :param redirect_uris: [url from client starting with https] - :type redirect_uris: list - :param additional_metadata: additional client metadata - :type additional_metadata: dict - """ - self.__additional_metadata = additional_metadata - self.clientAdapter = Client(client_authn_method=CLIENT_AUTHN_METHOD, message_factory=CustomMessageFactory) - self.__op_url = op_url - self.__redirect_uris = redirect_uris - self.__metadata_url = '%s/.well-known/openid-configuration' % op_url - self.op_data = self.discover(op_url) - self.reg_info = self.register_client(op_data=self.op_data, redirect_uris=redirect_uris) - self.__end_session_endpoint = self.op_data['end_session_endpoint'] - self.__client_id = self.reg_info['client_id'] - self.__client_secret = self.reg_info['client_secret'] - - def get_client_dict(self) -> dict: - r = { - 'op_metadata_url': self.__metadata_url, - 'client_id': self.__client_id, - 'client_secret': self.__client_secret, - 'end_session_endpoint': self.__end_session_endpoint - } - - return r - - def register_client(self, op_data: ASConfigurationResponse = op_data, redirect_uris: Optional[list[str]] = __redirect_uris) -> dict: - """[register client and returns client information] - - :param op_data: [description] - :type op_data: dict - :param redirect_uris: [description] - :type redirect_uris: list[str] - :return: [client information including client-id and secret] - :rtype: dict - """ - logger.debug('called ClientHandler´s register_client method') - registration_args = {'redirect_uris': redirect_uris, - 'response_types': ['code'], - 'grant_types': ['authorization_code'], - 'application_type': 'web', - 'client_name': 'Jans Tent', - 'token_endpoint_auth_method': 'client_secret_post', - **self.__additional_metadata - } - logger.info('calling register with registration_args: %s', json.dumps(registration_args, indent=2)) - reg_info = self.clientAdapter.register(op_data['registration_endpoint'], **registration_args) - logger.info('register_client - reg_info = %s', json.dumps(reg_info.to_dict(), indent=2)) - return reg_info - - def discover(self, op_url: Optional[str] = __op_url) -> ASConfigurationResponse: - """Discover op information on .well-known/open-id-configuration - :param op_url: [description], defaults to __op_url - :type op_url: str, optional - :return: [data retrieved from OP url] - :rtype: ASConfigurationResponse - """ - logger.debug('called discover') - try: - op_data = self.clientAdapter.provider_config(op_url) - return op_data - - except json.JSONDecodeError as err: - logger.error('Error trying to decode JSON: %s' % err) - - except RelativeURIError as err: - logger.error(err) - - except Exception as e: - logging.error('An unexpected ocurred: %s' % e) - diff --git a/demos/jans-tent/clientapp/helpers/custom_msg_factory.py b/demos/jans-tent/clientapp/helpers/custom_msg_factory.py deleted file mode 100644 index 11f0ae09a60..00000000000 --- a/demos/jans-tent/clientapp/helpers/custom_msg_factory.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Custom message factory required by pyoic to add scope param -Overrides RegistrationRequest, RegistrationResponse -and use them to create CustomMessageFactory -""" - -from oic.oic.message import OIDCMessageFactory, RegistrationRequest, RegistrationResponse, MessageTuple, OPTIONAL_LOGICAL -from oic.oauth2.message import OPTIONAL_LIST_OF_STRINGS, REQUIRED_LIST_OF_STRINGS, SINGLE_OPTIONAL_STRING, SINGLE_OPTIONAL_INT - - -class MyRegistrationRequest(RegistrationRequest): - c_param = { - "redirect_uris": REQUIRED_LIST_OF_STRINGS, - "response_types": OPTIONAL_LIST_OF_STRINGS, - "grant_types": OPTIONAL_LIST_OF_STRINGS, - "application_type": SINGLE_OPTIONAL_STRING, - "contacts": OPTIONAL_LIST_OF_STRINGS, - "client_name": SINGLE_OPTIONAL_STRING, - "logo_uri": SINGLE_OPTIONAL_STRING, - "client_uri": SINGLE_OPTIONAL_STRING, - "policy_uri": SINGLE_OPTIONAL_STRING, - "tos_uri": SINGLE_OPTIONAL_STRING, - "jwks": SINGLE_OPTIONAL_STRING, - "jwks_uri": SINGLE_OPTIONAL_STRING, - "sector_identifier_uri": SINGLE_OPTIONAL_STRING, - "subject_type": SINGLE_OPTIONAL_STRING, - "id_token_signed_response_alg": SINGLE_OPTIONAL_STRING, - "id_token_encrypted_response_alg": SINGLE_OPTIONAL_STRING, - "id_token_encrypted_response_enc": SINGLE_OPTIONAL_STRING, - "userinfo_signed_response_alg": SINGLE_OPTIONAL_STRING, - "userinfo_encrypted_response_alg": SINGLE_OPTIONAL_STRING, - "userinfo_encrypted_response_enc": SINGLE_OPTIONAL_STRING, - "request_object_signing_alg": SINGLE_OPTIONAL_STRING, - "request_object_encryption_alg": SINGLE_OPTIONAL_STRING, - "request_object_encryption_enc": SINGLE_OPTIONAL_STRING, - "token_endpoint_auth_method": SINGLE_OPTIONAL_STRING, - "token_endpoint_auth_signing_alg": SINGLE_OPTIONAL_STRING, - "default_max_age": SINGLE_OPTIONAL_INT, - "require_auth_time": OPTIONAL_LOGICAL, - "default_acr_values": OPTIONAL_LIST_OF_STRINGS, - "initiate_login_uri": SINGLE_OPTIONAL_STRING, - "request_uris": OPTIONAL_LIST_OF_STRINGS, - "post_logout_redirect_uris": OPTIONAL_LIST_OF_STRINGS, - "frontchannel_logout_uri": SINGLE_OPTIONAL_STRING, - "frontchannel_logout_session_required": OPTIONAL_LOGICAL, - "backchannel_logout_uri": SINGLE_OPTIONAL_STRING, - "backchannel_logout_session_required": OPTIONAL_LOGICAL, - "scope": OPTIONAL_LIST_OF_STRINGS, # added - } - c_default = {"application_type": "web", "response_types": ["code"]} - c_allowed_values = { - "application_type": ["native", "web"], - "subject_type": ["public", "pairwise"], - } - - -class CustomMessageFactory(OIDCMessageFactory): - registration_endpoint = MessageTuple(MyRegistrationRequest, RegistrationResponse) - diff --git a/demos/jans-tent/clientapp/templates/home.html b/demos/jans-tent/clientapp/templates/home.html deleted file mode 100644 index 021c6fcfaac..00000000000 --- a/demos/jans-tent/clientapp/templates/home.html +++ /dev/null @@ -1,21 +0,0 @@ - - Index Test - -

Welcome to the test of your life

-

- {% if user %} -

Userinfo JSON payload

-
-        {{ user|tojson }}
-        
-

-

id_token JSON payload

-
-        {{ id_token|tojson }}
-        
- logout - {% else %} -

Click here to start!

- {% endif %} - - \ No newline at end of file diff --git a/demos/jans-tent/clientapp/utils/__init__.py b/demos/jans-tent/clientapp/utils/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/clientapp/utils/dcr_from_config.py b/demos/jans-tent/clientapp/utils/dcr_from_config.py deleted file mode 100644 index 7ab19246abf..00000000000 --- a/demos/jans-tent/clientapp/utils/dcr_from_config.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import urllib.parse - -from clientapp import config as cfg -from clientapp.helpers.client_handler import ClientHandler -import json -from urllib import parse - -OP_URL = cfg.ISSUER -REDIRECT_URIS = cfg.REDIRECT_URIS -SCOPE = cfg.SCOPE -parsed_redirect_uri = urllib.parse.urlparse(cfg.REDIRECT_URIS[0]) -POST_LOGOUT_REDIRECT_URI = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - - -def setup_logging() -> None: - logging.getLogger('oic') - logging.getLogger('urllib3') - logging.basicConfig( - level=logging.DEBUG, - handlers=[logging.StreamHandler(), logging.FileHandler('register_new_client.log')], - format='[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s') - - -def register() -> None: - """ - Register client with information from config and write info to client_info.json - :return: None - """ - logger = logging.getLogger(__name__) - scope_as_list = SCOPE.split(" ") - additional_params = { - 'scope': scope_as_list, - 'post_logout_redirect_uris': [POST_LOGOUT_REDIRECT_URI] - } - client_handler = ClientHandler(OP_URL, REDIRECT_URIS, additional_params) - json_client_info = json.dumps(client_handler.get_client_dict(), indent=4) - with open('client_info.json', 'w') as outfile: - logger.info('Writing registered client information to client_info.json') - outfile.write(json_client_info) - diff --git a/demos/jans-tent/clientapp/utils/logger.py b/demos/jans-tent/clientapp/utils/logger.py deleted file mode 100644 index acbcc2ca7bf..00000000000 --- a/demos/jans-tent/clientapp/utils/logger.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -from logging.handlers import TimedRotatingFileHandler - - -def setup_logger() -> None: - formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s") - log_file = "test-client.log" - file_handler = TimedRotatingFileHandler(log_file, when='midnight') - console_handler = logging.StreamHandler() - console_handler.setFormatter(formatter) - file_handler.setFormatter(formatter) - logging.getLogger("oic") - logging.getLogger("oauth") - logging.getLogger("flask-oidc") - logging.getLogger("urllib3") - logging.basicConfig(level=logging.DEBUG, handlers=[file_handler, console_handler]) diff --git a/demos/jans-tent/docs/images/authorize_code_flow.png b/demos/jans-tent/docs/images/authorize_code_flow.png deleted file mode 100644 index a0c262649890154165f225d1e515e0b521ff9f2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53305 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 V$=j@=`}m(jJUx~#Dq1Mn{SW6T6ej=x diff --git a/demos/jans-tent/main.py b/demos/jans-tent/main.py deleted file mode 100644 index ebd89f31fd6..00000000000 --- a/demos/jans-tent/main.py +++ /dev/null @@ -1,6 +0,0 @@ -from clientapp import create_app - -if __name__ == '__main__': - app = create_app() - app.debug = True - app.run(host='0.0.0.0', ssl_context=('cert.pem', 'key.pem'), port=9090, use_reloader=False) diff --git a/demos/jans-tent/register_new_client.py b/demos/jans-tent/register_new_client.py deleted file mode 100644 index 89061c42275..00000000000 --- a/demos/jans-tent/register_new_client.py +++ /dev/null @@ -1,12 +0,0 @@ -# executes a new client auto-register from config.py -import logging -from clientapp.utils.dcr_from_config import register - -# add independent logging for CLI script -logging.getLogger('oic') -logging.getLogger('urllib3') -logging.basicConfig( - level=logging.DEBUG, - handlers=[logging.StreamHandler(), logging.FileHandler('register_new_client.log')], - format='[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s') -register() diff --git a/demos/jans-tent/requirements.txt b/demos/jans-tent/requirements.txt deleted file mode 100644 index f6438fdbaae..00000000000 --- a/demos/jans-tent/requirements.txt +++ /dev/null @@ -1,119 +0,0 @@ -appnope==0.1.3 -astroid==2.12.5 -asttokens==2.0.8 -async-generator==1.10 -attrs==22.1.0 -Authlib==1.2.0 -autopep8==1.7.0 -backcall==0.2.0 -bandit==1.7.4 -behave==1.2.6 -certifi==2022.12.7 -cffi==1.15.1 -chardet==5.0.0 -charset-normalizer==2.1.1 -click==8.1.3 -coverage==6.4.4 -cryptography==42.0.0 -decorator==5.1.1 -defusedxml==0.7.1 -dill==0.3.5.1 -dodgy==0.2.1 -EasyProcess==1.1 -executing==1.0.0 -flake8==5.0.4 -Flask==2.2.2 -flask-oidc==1.4.0 -future==0.18.3 -gitdb==4.0.9 -GitPython==3.1.37 -h11==0.13.0 -httplib2==0.21.0 -idna==3.3 -importlib-metadata==4.12.0 -iniconfig==1.1.1 -install==1.3.5 -ipdb==0.13.9 -ipython==8.10.0 -ipython-genutils==0.2.0 -isort==5.10.1 -itsdangerous==2.0.0 -jedi==0.18.1 -Jinja2==3.1.2 -lazy-object-proxy==1.7.1 -Mako==1.2.4 -MarkupSafe==2.1.1 -matplotlib-inline==0.1.6 -mccabe==0.7.0 -more-itertools==8.14.0 -mypy==0.971 -mypy-extensions==0.4.3 -oauth2client==4.1.3 -oic==1.5.0 -outcome==1.2.0 -packaging==21.3 -parse==1.19.0 -parse-type==0.6.0 -parso==0.8.3 -pbr==5.10.0 -pep8==1.7.1 -pep8-naming==0.13.2 -pexpect==4.8.0 -pickleshare==0.7.5 -platformdirs==2.5.2 -pluggy==1.0.0 -poetry-semver==0.1.0 -prompt-toolkit==3.0.31 -prospector==0.12.2 -ptyprocess==0.7.0 -pure-eval==0.2.2 -py==1.11.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycodestyle==2.9.1 -pycparser==2.21 -pycryptodomex==3.17 -pydocstyle==6.1.1 -pyflakes==2.5.0 -Pygments==2.13.0 -pyjwkest==1.4.2 -pylama==8.4.1 -pylint==2.15.0 -pylint-celery==0.3 -pylint-common==0.2.5 -pylint-django==2.5.3 -pylint-flask==0.6 -pylint-plugin-utils==0.7 -pyOpenSSL==22.0.0 -pyparsing==3.0.9 -PySocks==1.7.1 -pytest==7.1.3 -python-dotenv==0.21.0 -PyVirtualDisplay==3.0 -PyYAML==6.0 -requests==2.28.1 -requirements-detector==1.0.3 -rsa==4.9 -selenium==4.4.3 -setoptconf==0.3.0 -six==1.16.0 -smmap==5.0.0 -sniffio==1.3.0 -snowballstemmer==2.2.0 -sortedcontainers==2.4.0 -stack-data==0.5.0 -stevedore==4.0.0 -toml==0.10.2 -tomli==2.0.1 -tomlkit==0.11.4 -traitlets==5.3.0 -trio==0.21.0 -trio-websocket==0.9.2 -typed-ast==1.5.4 -typing_extensions==4.3.0 -urllib3==1.26.12 -wcwidth==0.2.5 -Werkzeug==2.2.2 -wrapt==1.14.1 -wsproto==1.2.0 -zipp==3.8.1 diff --git a/demos/jans-tent/tests/behaver/features/environment.py b/demos/jans-tent/tests/behaver/features/environment.py deleted file mode 100644 index 037be43286f..00000000000 --- a/demos/jans-tent/tests/behaver/features/environment.py +++ /dev/null @@ -1,31 +0,0 @@ -from selenium import webdriver -import os -from pyvirtualdisplay import Display - -display = Display(visible=0, size=(1024, 768)) - - -def before_all(context): - os.environ['CURL_CA_BUNDLE'] = "" - display.start() - - -def before_scenario(context, scenario): - options = webdriver.FirefoxOptions() - options.headless = True - context.web = webdriver.Firefox() - - # context.web = webdriver.Firefox() - - -def after_scenario(context, scenario): - context.web.delete_all_cookies() - context.web.close() - - -def after_step(context, step): - print() - - -def after_all(context): - pass diff --git a/demos/jans-tent/tests/behaver/features/oidc_auth.feature b/demos/jans-tent/tests/behaver/features/oidc_auth.feature deleted file mode 100644 index b95f5df3e61..00000000000 --- a/demos/jans-tent/tests/behaver/features/oidc_auth.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: Allow authenticated users to access protected pages - - @authenticated - Scenario: User is authenticated - Given username is "johndoe" - And user is authenticated - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user access the protected content link - - Scenario: User does not exist - Given user does not exist - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user goes to external login page - - Scenario: User is not authenticated - Given username is "johndoe" - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user goes to external login page - - # Scenario: Normal user try to access admin content - # Given username is "johndoe" - # And user role is "user" - # And protected content link is https://chris.testingenv.org/admin/admin-protected-content - # When user clicks the protected content link - # Then user gets a 403 error - - # Scenario: Admin can access admin contents - # Given username is "johndoe" - # And user role is "admin" - # And protected content link is https://chris.testingenv.org/admin/admin-protected-content - # When user clicks the protected content link - # Then user access the protected content link - - - - - diff --git a/demos/jans-tent/tests/behaver/features/passport_social_auth.feature b/demos/jans-tent/tests/behaver/features/passport_social_auth.feature deleted file mode 100644 index 685576147f2..00000000000 --- a/demos/jans-tent/tests/behaver/features/passport_social_auth.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: use passport social github to login - """ - As an user, - I want to use passport-social flow to authenticate - So I can access protected-content - """ - - Background: - Given auth method is passport-social - And user is visiting "/" - - Scenario: User is authenticated - Given username is "johndoe" - And protected content link is https://localhost:5000/content/protected-user-content - When user clicks the protected content link - Then user access the protected content link - - Scenario: User is not authenticated - Given user is not authenticated - When user clicks the protected content link - Then user goes to external login page - - - - - \ No newline at end of file diff --git a/demos/jans-tent/tests/behaver/features/steps/allow.py b/demos/jans-tent/tests/behaver/features/steps/allow.py deleted file mode 100644 index 97a6ddfdb4a..00000000000 --- a/demos/jans-tent/tests/behaver/features/steps/allow.py +++ /dev/null @@ -1,116 +0,0 @@ -from behave import when, then, given -import requests -import time -from selenium.webdriver.common.by import By - -base_url = "https://chris.testingenv.org" - - -def cookiesTransformer(sel_session_id, sel_other_cookies): - ''' This transform cookies from selenium to requests ''' - s = requests.Session() - s.cookies.set('session_id', sel_session_id) - i = 0 - while i < len(sel_other_cookies): - s.cookies.set(sel_other_cookies[i]['name'], - sel_other_cookies[i]['value'], - path=sel_other_cookies[i]['path'], - domain=sel_other_cookies[i]['domain'], - secure=sel_other_cookies[i]['secure'], - rest={'httpOnly': sel_other_cookies[i]['httpOnly']}) - i = i + 1 - - return s - - -@given(u'username is "{username}"') -def define_username(context, username): - context.username = username - context.password = "test123" - - -@given(u'user is authenticated') -def user_authenticates(context): - context.web.get("https://chris.testingenv.org/login") - time.sleep(3) - context.web.set_window_size(625, 638) - context.web.find_element(By.ID, "username").click() - context.web.find_element(By.ID, "username").send_keys("johndoo") - time.sleep(3) - context.web.find_element(By.ID, "password").send_keys("test123") - context.web.find_element(By.ID, "loginButton").click() - time.sleep(3) - - -@given(u'protected content link is {protected_content}') -def define_protected_content_link(context, protected_content): - context.protected_content = protected_content - - -@when(u'user clicks the protected content link') -def user_clicks_protected_content_link(context): - - context.web.get(base_url) - time.sleep(2) - context.web.find_element_by_xpath( - '//a[@href="' + "https://chris.testingenv.org/protected-content" + - '"]').click() - context.has_clicked = True - context.response = requests.get(context.protected_content) - - -@then(u'user access the protected content link') -def user_access_protected_content_link(context): - # WE FETCH THE COOKIES FROM SELENIUM AND PASS THEM TO REQUESTS TO VALIDATE - #sel_cookies = context.web.get_cookies() - #sel_cookie = sel_cookies[0] - # set cookie in requests - - # get session id from selenium - #sel_session_id = context.web.session_id - ''' - sess = requests.Session() - - sess.cookies.set('session_id',sel_session_id) - sess.cookies.set( - sel_cookie['name'], - sel_cookie['value'], - path = sel_cookie['path'], - domain = sel_cookie['domain'], - secure = sel_cookie['secure'], - rest= {'httpOnly' : sel_cookie['httpOnly']} - ) - - new_sess = cookiesTransformer(sel_session_id,sel_cookies) - ''' - new_sess = cookiesTransformer(context.web.session_id, - context.web.get_cookies()) - res = new_sess.get(context.protected_content, verify=False) - - assert res.url == context.protected_content - - -@given(u'user does not exist') -def user_does_not_exist(context): - pass - - -@then(u'user goes to external login page') -def user_directed_to_external_login_page(context): - #context.web.get("https://chris.testingenv.org/login") - - time.sleep(1) - external_login_url = 'https://chris.gluutwo.org/oxauth/login.htm' - #import ipdb; ipdb.set_trace() - assert (context.web.current_url == external_login_url) - #new_sess = cookiesTransformer(context.web.session_id,context.web.get_cookies()) - - -@given(u'user role is "{role}"') -def define_user_role(context, role): - context.role = role - - -@then(u'user gets a 403 error') -def step_impl(context): - raise NotImplementedError(u'STEP: Then user gets a 403 error') diff --git a/demos/jans-tent/tests/unit_integration/helper.py b/demos/jans-tent/tests/unit_integration/helper.py deleted file mode 100644 index b282f4b64ab..00000000000 --- a/demos/jans-tent/tests/unit_integration/helper.py +++ /dev/null @@ -1,189 +0,0 @@ - -from unittest import TestCase -from unittest.mock import MagicMock -import clientapp -from clientapp import create_app -from clientapp.helpers.client_handler import ClientHandler -from flask import Flask -from typing import List -import helper -import os -import builtins - - -class FlaskBaseTestCase(TestCase): - def setUp(self): - self.stashed_add_config_from_json = clientapp.add_config_from_json - clientapp.cfg.CLIENT_ID = 'any-client-id-stub' - clientapp.cfg.CLIENT_SECRET = 'any-client-secret-stub' - clientapp.cfg.SERVER_META_URL = 'https://ophostname.com/server/meta/url' - clientapp.cfg.END_SESSION_ENDPOINT = 'https://ophostname.com/end_session_endpoint' - clientapp.add_config_from_json = MagicMock(name='add_config_from_json') - clientapp.add_config_from_json.return_value(None) - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - builtins.open = MagicMock(name='open') - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - self.app = create_app() - self.app.testing = True - self.app_context = self.app.test_request_context( - base_url="https://chris.testingenv.org") - self.app_context.push() - self.client = self.app.test_client() - - #self.oauth = OAuth(self.app) - os.environ['AUTHLIB_INSECURE_TRANSPORT'] = "1" - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - clientapp.add_config_from_json = self.stashed_add_config_from_json - - -# Helper functions -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -# Mocks -OP_DATA_DICT_RESPONSE = { - 'request_parameter_supported': True, - 'token_revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke', - 'introspection_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/introspection', - 'claims_parameter_supported': False, - 'issuer': 'https://t1.techno24x7.com', - 'userinfo_encryption_enc_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'id_token_encryption_enc_values_supported': ['A128CBC+HS256', 'A256CBC+HS512', 'A128GCM', 'A256GCM'], - 'authorization_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/authorize', - 'service_documentation': 'http://gluu.org/docs', - 'id_generation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/id', - 'claims_supported': ['street_address', 'country', 'zoneinfo', 'birthdate', 'role', 'gender', 'formatted', - 'user_name', 'phone_mobile_number', 'preferred_username', 'locale', 'inum', 'updated_at', - 'nickname', 'email', 'website', 'email_verified', 'profile', 'locality', - 'phone_number_verified', 'given_name', 'middle_name', 'picture', 'name', 'phone_number', - 'postal_code', 'region', 'family_name'], - 'scope_to_claims_mapping': [{ - 'profile': ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', - 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'] - }, { - 'openid': [] - }, { - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/scim_access': [] - }, { - 'permission': ['role'] - }, { - 'super_gluu_ro_session': [] - }, { - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/passport_access': [] - }, { - 'phone': ['phone_number_verified', 'phone_number'] - }, { - 'revoke_session': [] - }, { - 'address': ['formatted', 'postal_code', 'street_address', 'locality', 'country', 'region'] - }, { - 'clientinfo': ['name', 'inum'] - }, { - 'mobile_phone': ['phone_mobile_number'] - }, { - 'email': ['email_verified', 'email'] - }, { - 'user_name': ['user_name'] - }, { - 'oxtrust-api-write': [] - }, { - 'oxd': [] - }, { - 'uma_protection': [] - }, { - 'oxtrust-api-read': [] - }], - 'op_policy_uri': 'http://ox.gluu.org/doku.php?id=oxauth:policy', - 'token_endpoint_auth_methods_supported': ['client_secret_basic', 'client_secret_post', 'client_secret_jwt', - 'private_key_jwt', 'tls_client_auth', 'self_signed_tls_client_auth'], - 'tls_client_certificate_bound_access_tokens': True, - 'response_modes_supported': ['query', 'form_post', 'fragment'], - 'backchannel_logout_session_supported': True, - 'token_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/token', - 'response_types_supported': ['code id_token', 'code', 'id_token', 'token', 'code token', 'code id_token token', - 'id_token token'], - 'request_uri_parameter_supported': True, - 'backchannel_user_code_parameter_supported': False, - 'grant_types_supported': ['implicit', 'refresh_token', 'client_credentials', 'authorization_code', 'password', - 'urn:ietf:params:oauth:grant-type:uma-ticket'], - 'ui_locales_supported': ['en', 'bg', 'de', 'es', 'fr', 'it', 'ru', 'tr'], - 'userinfo_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/userinfo', - 'op_tos_uri': 'http://ox.gluu.org/doku.php?id=oxauth:tos', - 'auth_level_mapping': { - '-1': ['simple_password_auth'], - '60': ['passport_saml'], - '40': ['passport_social'] - }, - 'require_request_uri_registration': False, - 'id_token_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'frontchannel_logout_session_supported': True, - 'claims_locales_supported': ['en'], - 'clientinfo_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/clientinfo', - 'request_object_signing_alg_values_supported': ['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', - 'ES256', 'ES384', 'ES512'], - 'request_object_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'session_revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke_session', - 'check_session_iframe': 'https://t1.techno24x7.com/oxauth/opiframe.htm', - 'scopes_supported': ['address', 'openid', 'clientinfo', 'user_name', 'profile', - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/scim_access', 'uma_protection', - 'permission', 'revoke_session', 'oxtrust-api-write', 'oxtrust-api-read', 'phone', - 'mobile_phone', 'oxd', 'super_gluu_ro_session', 'email', - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/passport_access'], - 'backchannel_logout_supported': True, - 'acr_values_supported': ['simple_password_auth', 'passport_saml', 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', 'passport_social'], - 'request_object_encryption_enc_values_supported': ['A128CBC+HS256', 'A256CBC+HS512', 'A128GCM', 'A256GCM'], - 'display_values_supported': ['page', 'popup'], - 'userinfo_signing_alg_values_supported': ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', - 'ES512'], - 'claim_types_supported': ['normal'], - 'userinfo_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'end_session_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/end_session', - 'revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke', - 'backchannel_authentication_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/bc-authorize', - 'token_endpoint_auth_signing_alg_values_supported': ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', - 'ES384', 'ES512'], - 'frontchannel_logout_supported': True, - 'jwks_uri': 'https://t1.techno24x7.com/oxauth/restv1/jwks', - 'subject_types_supported': ['public', 'pairwise'], - 'id_token_signing_alg_values_supported': ['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', - 'ES384', 'ES512'], - 'registration_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/register', - 'id_token_token_binding_cnf_values_supported': ['tbh'] -} - -REGISTER_CLIENT_RESPONSE = {'allow_spontaneous_scopes': False, 'application_type': 'web', 'rpt_as_jwt': False, - 'registration_client_uri': 'https://t1.techno24x7.com/jans-auth/restv1/register?client_id' - '=079f3682-3d60-4bca-8ff7-bbc7dbc18cd7', - 'run_introspection_script_before_jwt_creation': False, - 'registration_access_token': '89c51fd6-34ec-497e-a4ae-85e21b7e725b', - 'client_id': '079f3682-3d60-4bca-8ff7-bbc7dbc18cd7', - 'token_endpoint_auth_method': 'client_secret_post', - 'scope': 'online_access device_sso openid permission uma_protection offline_access', - 'client_secret': '8f53c454-f6ee-4181-8581-9f1ee120b878', 'client_id_issued_at': 1680038429, - 'backchannel_logout_session_required': False, 'client_name': 'Jans Tent', - 'par_lifetime': 600, 'spontaneous_scopes': [], 'id_token_signed_response_alg': 'RS256', - 'access_token_as_jwt': False, 'grant_types': ['authorization_code'], - 'subject_type': 'pairwise', 'additional_token_endpoint_auth_methods': [], - 'keep_client_authorization_after_expiration': False, 'require_par': False, - 'redirect_uris': ['https://localhost:9090/oidc_callback'], 'additional_audience': [], - 'frontchannel_logout_session_required': False, 'client_secret_expires_at': 0, - 'access_token_signing_alg': 'RS256', 'response_types': ['code']} - - diff --git a/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py b/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py deleted file mode 100644 index 91ebb43edca..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py +++ /dev/null @@ -1,62 +0,0 @@ -import clientapp -from flask import Flask, url_for -from typing import List -from helper import FlaskBaseTestCase - - -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -# class FlaskBaseTestCase(TestCase): -# def setUp(self): -# self.app = clientapp.create_app() -# self.app.testing = True -# self.app_context = self.app.test_request_context( -# base_url="https://chris.testingenv.org") -# self.app_context.push() -# self.client = self.app.test_client() -# #self.oauth = OAuth(self.app) -# os.environ['AUTHLIB_INSECURE_TRANSPORT'] = "1" - - -class TestCallbackEndpoint(FlaskBaseTestCase): - def test_oidc_callback_endpoint_exist(self): - endpoints = [] - for item in clientapp.create_app().url_map.iter_rules(): - endpoint = item.rule - endpoints.append(endpoint) - - self.assertTrue('/oidc_callback' in endpoints, - "enpoint /oidc_callback knão existe no app") - - def test_callback_endpoint_should_exist(self): - - self.assertTrue('callback' in app_endpoints(clientapp.create_app()), - 'endpoint /callback does not exist in app') - - def test_endpoint_args_without_code_should_return_400(self): - resp = self.client.get(url_for('callback')) - - self.assertEqual(resp.status_code, 400) - - -''' - def test_endpoint_should_return_status_code_302(self): - # if there is - - self.assertEqual( - self.client.get(url_for('callback')).status_code, - 302, - 'Callback endpoint is not returning 302 status_code' - ) - - - #def test_endpoint_should_return_ -''' diff --git a/demos/jans-tent/tests/unit_integration/test_cfg_checker.py b/demos/jans-tent/tests/unit_integration/test_cfg_checker.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py b/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py deleted file mode 100644 index 83aef096738..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py +++ /dev/null @@ -1,145 +0,0 @@ -from helper import FlaskBaseTestCase -import clientapp -import helper -from flask import url_for -from clientapp.helpers.client_handler import ClientHandler -from unittest.mock import MagicMock, patch - - -class TestRegisterEndpoint(FlaskBaseTestCase): - - def test_if_app_has_register_endpoint(self): - self.assertIn( - 'register', - helper.app_endpoints(clientapp.create_app()) - ) - - def test_if_endpoint_accepts_post(self): - methods = None - for rule in self.app.url_map.iter_rules('register'): - methods = rule.methods - self.assertIn( - 'POST', - methods - ) - - # def test_init_should_call_discover_once(self): - # ClientHandler.discover = MagicMock(name='discover') - # ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - # ClientHandler.discover.assert_called_once() - - def test_endpoint_should_return_valid_req(self): - self.assertIn( - self.client.post(url_for('register')).status_code, - range(100, 511), - '/register returned invalid requisition' - ) - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_init_client_handler(self): - self.client.post(url_for('register'), json={ - 'op_url': 'https://test.com', - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - ClientHandler.__init__.assert_called_once() - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_accept_2_params(self): - first_value = 'https://op' - second_value = ['https://client.com.br/oidc_callback'] - - self.client.post(url_for('register'), json={ - 'op_url': first_value, - 'redirect_uris': second_value - }) - ClientHandler.__init__.assert_called_once_with(first_value, second_value, {}) - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_accept_3_params(self): - first_value = 'https://op' - second_value = ['https://client.com.br/oidc_callback'] - third_value = {'scope': 'openid email profile'} - - self.client.post(url_for('register'), json={ - 'op_url': first_value, - 'redirect_uris': second_value, - 'additional_params': third_value - }) - - ClientHandler.__init__.assert_called_once_with(first_value, second_value, third_value) - - def test_endpoint_should_return_error_code_400_if_no_data_sent(self): - self.assertEqual( - self.client.post(url_for('register')).status_code, - 400, - 'status_code for empty request is NOT 400' - ) - - def test_should_return_400_error_if_no_needed_keys_provided(self): - self.assertEqual( - self.client.post(url_for('register'), json={ - 'other_key': 'othervalue', - 'another_key': 'another_value' - }).status_code, - 400, - 'not returning 400 code if no needed keys provided' - ) - - def test_should_return_400_if_values_are_not_valid_urls(self): - self.assertEqual( - self.client.post(url_for('register'), json={ - 'op_url': 'not_valid_url', - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }).status_code, - 400, - 'not returning status 400 if values are not valid urls' - ) - - @patch('clientapp.helpers.client_handler.ClientHandler.get_client_dict', MagicMock(return_value=None)) - def test_valid_post_should_should_call_get_client_dict_once(self): - op_url = 'https://op.com.br' - self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - ClientHandler.get_client_dict.assert_called_once() - - def test_should_should_return_200_if_registered(self): - op_url = 'https://op.com.br' - test_client_id = '1234-5678-9ten11' - test_client_secret = 'mysuperprotectedsecret' - with patch.object(ClientHandler, 'get_client_dict', return_value={ - 'op_metadata_url': '%s/.well-known/open-id-configuration' % op_url, - 'client_id': test_client_id, - 'client_secret': test_client_secret - }) as get_client_dict: - response = self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - self.assertEqual(response.status_code, 200) - get_client_dict.reset() - - def test_should_return_expected_keys(self): - op_url = 'https://op.com.br' - redirect_uris = ['https://client.com.br/oidc_calback'] - test_client_id = '1234-5678-9ten11' - test_client_secret = 'mysuperprotectedsecret' - additional_params = {'param1': 'value1'} - - expected_keys = {'op_metadata_url', 'client_id', 'client_secret'} - - with patch.object(ClientHandler, 'get_client_dict', return_value={ - 'op_metadata_url': '%s/.well-known/open-id-configuration' % op_url, - 'client_id': test_client_id, - 'client_secret': test_client_secret - }) as get_client_dict: - response = self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': redirect_uris, - 'additional_params': additional_params - }) - print(response) - assert expected_keys <= response.json.keys(), response.json - - get_client_dict.reset() diff --git a/demos/jans-tent/tests/unit_integration/test_config.py b/demos/jans-tent/tests/unit_integration/test_config.py deleted file mode 100644 index 419e3bd56a9..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_config.py +++ /dev/null @@ -1,19 +0,0 @@ -import clientapp.config as cfg -from unittest import TestCase - - -class TestConfig(TestCase): - def test_has_attribute_SSL_VERIFY(self): - self.assertTrue(hasattr(cfg, 'SSL_VERIFY'), - 'SSL_VERIFY attribute is missing in config.') - - def test_SSL_VERIFY_has_boolean_value(self): - self.assertTrue('__bool__' in cfg.SSL_VERIFY.__dir__(), - 'SSL_VERIFY is not boolean.') - - def test_has_attribute_SCOPE(self): - self.assertTrue(hasattr(cfg, 'SCOPE'), - 'SCOPE attribute is missing in config.') - - def test_SCOPE_default_should_be_openid(self): - self.assertTrue(cfg.SCOPE == 'openid') diff --git a/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py b/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py deleted file mode 100644 index 1890b1d4cee..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py +++ /dev/null @@ -1,107 +0,0 @@ -import clientapp -from flask import Flask, url_for -from typing import List -import json -from helper import FlaskBaseTestCase - - -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -def valid_client_configuration(): - return { - "client_id": "my-client-id", - "client_secret": "my-client-secret", - "op_metadata_url": "https://op.com/.well-known/openidconfiguration" - } - - -class TestConfigurationEndpoint(FlaskBaseTestCase): - def test_create_app_has_configuration(self): - self.assertTrue( - 'configuration' in app_endpoints(clientapp.create_app()), - 'endpoint /configuration does not exist in app') - - def test_configuration_endpoint_should_return_valid_req(self): - self.assertIn( - self.client.post(url_for('configuration')).status_code, - range(100, 511), '/configuration returned invalid requisition') - - def test_endpoint_should_return_200_if_valid_json(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - response = self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - self.assertEqual(response.status_code, 200) - - def test_endpoint_should_return_posted_data_if_valid_json(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - response = self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertEqual(json_data, json.dumps(response.json)) - - def test_endpoint_should_setup_cfg_with_provider_id(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertEqual(clientapp.cfg.PRE_SELECTED_PROVIDER_ID, 'whatever') - - def test_endpoint_should_setup_cfg_with_pre_selected_provider_true(self): - clientapp.cfg.PRE_SELECTED_PROVIDER = False - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertTrue(clientapp.cfg.PRE_SELECTED_PROVIDER, ) - - def test_endpoint_should_return_200_if_valid_client_config(self): - headers = {'Content-type': 'application/json'} - json_data = json.dumps(valid_client_configuration()) - response = self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertEqual(response.status_code, 200, - 'endpoint is NOT returning 200 for valid client configuration') - - def test_endpoint_should_register_new_oauth_client_id(self): - headers = {'Content-type': 'application/json'} - client_id = "my-client-id" - client_secret = "my-client-secret" - op_metadata_url = "https://op.com/.well-known/openidconfiguration" - json_data = json.dumps({ - "client_id": client_id, - "client_secret": client_secret, - "op_metadata_url": op_metadata_url - }) - self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertTrue(clientapp.oauth.op.client_id == client_id, - 'endpoint is NOT changing op.client_id') - - def test_endpoint_should_register_new_oauth_client_secret(self): - headers = {'Content-type': 'application/json'} - json_data = json.dumps(valid_client_configuration()) - client_secret = valid_client_configuration()['client_secret'] - self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertTrue(clientapp.oauth.op.client_secret == client_secret, - '%s is is not %s' % (clientapp.oauth.op.client_secret, client_secret)) diff --git a/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py b/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py deleted file mode 100644 index e02f909ea28..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py +++ /dev/null @@ -1,76 +0,0 @@ -from clientapp.utils import dcr_from_config -from clientapp import config as cfg -from unittest.mock import MagicMock, patch, mock_open -from unittest import TestCase -from clientapp.helpers.client_handler import ClientHandler -import helper -import json -import builtins - -class TestDrcFromConfig(TestCase): - - def setUp(self) -> None: - # stashing to restore on teardown - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - builtins.open = MagicMock(name='open') - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - - def test_if_setup_logging_exists(self): - assert hasattr(dcr_from_config, 'setup_logging') - - def test_if_static_variables_exists(self): - assert hasattr(dcr_from_config, 'OP_URL') - assert hasattr(dcr_from_config, 'REDIRECT_URIS') - - def test_if_static_variables_from_config(self): - assert dcr_from_config.OP_URL == cfg.ISSUER - assert dcr_from_config.REDIRECT_URIS == cfg.REDIRECT_URIS - - def test_register_should_be_calable(self): - assert callable(dcr_from_config.register), 'not callable' - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_register_should_call_ClientHandler(self): - dcr_from_config.register() - ClientHandler.__init__.assert_called_once() - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_register_should_call_ClientHandler_with_params(self): - dcr_from_config.register() - ClientHandler.__init__.assert_called_once_with( - cfg.ISSUER, cfg.REDIRECT_URIS, { - 'scope': cfg.SCOPE.split(" "), - "post_logout_redirect_uris": ['https://localhost:9090'] - } - ) - - def test_register_should_call_open(self): - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - - open_mock.assert_called_once() - - def test_register_should_call_open_with_correct_params(self): - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - open_mock.assert_called_once_with('client_info.json', 'w') - - def test_register_should_call_write_with_client_info(self): - client = ClientHandler(cfg.ISSUER, cfg.REDIRECT_URIS, {}) - expected_json_client_info = json.dumps(client.get_client_dict(), indent=4) - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - open_mock_handler = open_mock() - open_mock_handler.write.assert_called_once_with(expected_json_client_info) - - diff --git a/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py b/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py deleted file mode 100644 index 0f51b7cb596..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py +++ /dev/null @@ -1,277 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock -import inspect - -import clientapp.helpers.client_handler as client_handler -from typing import Optional -import helper -from oic.oauth2 import ASConfigurationResponse - - -ClientHandler = client_handler.ClientHandler - -# helper -def get_class_instance(op_url='https://t1.techno24x7.com', - client_url='https://mock.test.com', - additional_metadata={}): - client_handler_obj = ClientHandler(op_url, client_url, additional_metadata) - return client_handler_obj - - -class TestDynamicClientRegistration(TestCase): - - def setUp(self) -> None: - self.register_client_stash = ClientHandler.register_client - self.discover_stash = ClientHandler.discover - - @staticmethod - def mock_methods(): - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - - def restore_stashed_mocks(self): - ClientHandler.discover = self.discover_stash - ClientHandler.register_client = self.register_client_stash - - def test_if_json_exists(self): - self.assertTrue(hasattr(client_handler, 'json'), - 'json does not exists in client_handler') - - def test_if_json_is_from_json_package(self): - self.assertTrue(client_handler.json.__package__ == 'json', - 'json is not from json') - - # testing ClientHandler class - def test_if_ClientHandler_is_class(self): - self.assertTrue(inspect.isclass(ClientHandler)) - - def test_if_register_client_exists(self): - self.assertTrue(hasattr(ClientHandler, 'register_client'), - 'register_client does not exists in ClientHandler') - - def test_if_register_client_is_callable(self): - self.assertTrue(callable(ClientHandler.register_client), - 'register_client is not callable') - - def test_if_register_client_receives_params(self): - expected_args = ['self', 'op_data', 'redirect_uris'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.register_client).args == expected_args, - 'register_client does not receive expected args') - - def test_if_register_client_params_are_expected_type(self): - insp = inspect.getfullargspec(ClientHandler.register_client) - self.assertTrue( - insp.annotations['op_data'] == ASConfigurationResponse - and insp.annotations['redirect_uris'] == Optional[list[str]], - 'register_client is not receiving the right params') - - def test_if_class_has_initial_expected_attrs(self): - initial_expected_attrs = [ - '_ClientHandler__client_id', - '_ClientHandler__client_secret', - '_ClientHandler__redirect_uris', - '_ClientHandler__metadata_url', - '_ClientHandler__additional_metadata', - 'discover', # method - 'register_client' # method - ] - - self.assertTrue( - all(attr in ClientHandler.__dict__.keys() - for attr in initial_expected_attrs), - 'ClientHandler does not have initial attrs') - - def test_if_discover_exists(self): - self.assertTrue(hasattr(ClientHandler, 'discover'), - 'discover does not exists in ClientHandler') - - def test_if_discover_is_callable(self): - self.assertTrue(callable(ClientHandler.discover), - 'discover is not callable') - - def test_if_discover_receives_params(self): - expected_args = ['self', 'op_url'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.discover).args == expected_args, - 'discover does not receive expected args') - - def test_if_discover_params_are_expected_type(self): - insp = inspect.getfullargspec(ClientHandler.discover) - self.assertTrue( - insp.annotations['op_url'] == Optional[str], - 'discover is not receiving the right params') - - def test_discover_should_return_valid_dict(self): - """[Checks if returns main keys] - """ - - main_keys = { - 'issuer', 'authorization_endpoint', 'token_endpoint', - 'userinfo_endpoint', 'clientinfo_endpoint', - 'session_revocation_endpoint', 'end_session_endpoint', - 'revocation_endpoint', 'registration_endpoint' - } - - self.mock_methods() - op_data = ClientHandler.discover(ClientHandler, - 'https://t1.techno24x7.com') - self.assertTrue(main_keys <= set(op_data), - 'discovery return data does not have main keys') - self.restore_stashed_mocks() - - def test_if_get_client_dict_exists(self): - self.assertTrue(hasattr(ClientHandler, 'get_client_dict'), - 'get_client_dict does not exists in ClientHandler') - - def test_if_get_client_dict_is_callable(self): - self.assertTrue(callable(ClientHandler.get_client_dict), - 'get_client_dict is not callable') - - def test_if_get_client_dict_receives_params(self): - expected_args = ['self'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.get_client_dict).args == expected_args, - 'get_client_dict does not receive expected args') - - def test_client_id_should_return_something(self): - self.assertIsNotNone( - ClientHandler.get_client_dict(ClientHandler), - 'get_client_dict returning NoneType. It has to return something!') - - def test_get_client_dict_should_return_a_dict(self): - self.assertIsInstance(ClientHandler.get_client_dict(ClientHandler), - dict, 'get_client_dict is not returning a dict') - - def test_class_init_should_set_op_url(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - - client_handler_obj = get_class_instance(op_url) - - self.restore_stashed_mocks() - - self.assertEqual(client_handler_obj.__dict__['_ClientHandler__op_url'], - op_url) - - def test_class_init_should_set_redirect_uris(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - redirect_uris = 'https://mock.test.com/oidc_callback' - client_handler_obj = ClientHandler(op_url, redirect_uris, {}) - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__redirect_uris'], - redirect_uris) - - def test_class_init_should_set_metadata_url(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - - client_handler_obj = get_class_instance(op_url) - - self.restore_stashed_mocks() - - expected_metadata_url = op_url + '/.well-known/openid-configuration' - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__metadata_url'], - expected_metadata_url) - - def test_class_init_should_set_additional_params(self): - self.mock_methods() - expected_metadata = {'metakey1': 'meta value 1'} - client_handler_obj = get_class_instance(additional_metadata=expected_metadata) - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__additional_metadata'], - expected_metadata - ) - - def test_class_init_should_have_docstring(self): - self.assertTrue(ClientHandler.__init__.__doc__, - 'ClientHandler.__init__ has doc') - - def test_if_get_client_dict_return_expected_keys(self): - expected_keys = [ - 'op_metadata_url', - 'client_id', - 'client_secret', - ] - - self.mock_methods() - - client_handler_obj = get_class_instance() - client_dict = client_handler_obj.get_client_dict() - - self.restore_stashed_mocks() - - self.assertTrue( - all(key in client_dict.keys() for key in expected_keys), - 'there is no %s IN %s: get_client_dict is NOT returning expected keys' - % (str(expected_keys), str(client_dict.keys()))) - - def test_get_client_dict_values_cannot_be_none(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - client_handler_obj = get_class_instance(op_url) - client_dict = client_handler_obj.get_client_dict() - - self.restore_stashed_mocks() - - for key in client_dict.keys(): - self.assertIsNotNone(client_dict[key], - 'get_client_dict[%s] cannot be None!' % key) - - def test_get_client_dict_should_return_url_metadata_value(self): - self.mock_methods() - - client_handler_obj = get_class_instance() - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.get_client_dict()['op_metadata_url'], - client_handler_obj._ClientHandler__metadata_url) - - def test_get_client_dict_should_return_client_id_value(self): - self.mock_methods() - - client_handler_obj = get_class_instance() - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.get_client_dict()['client_id'], - client_handler_obj._ClientHandler__client_id - ) - - def test_init_should_call_discover_once(self): - self.mock_methods() - - get_class_instance() - - ClientHandler.discover.assert_called_once() - - self.restore_stashed_mocks() - - def test_init_should_call_register_client_once(self): - self.mock_methods() - - get_class_instance() - ClientHandler.register_client.assert_called_once() - - self.restore_stashed_mocks() - diff --git a/demos/jans-tent/tests/unit_integration/test_flask_factory.py b/demos/jans-tent/tests/unit_integration/test_flask_factory.py deleted file mode 100644 index 0813c3b1ad1..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_flask_factory.py +++ /dev/null @@ -1,93 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock -import clientapp -from flask import Flask -import os -import builtins -from clientapp.helpers.client_handler import ClientHandler -import helper - - -class TestFlaskApp(TestCase): - - def setUp(self) -> None: - self.stashed_add_config_from_json = clientapp.add_config_from_json - clientapp.cfg.CLIENT_ID = 'any-client-id-stub' - clientapp.cfg.CLIENT_SECRET = 'any-client-secret-stub' - clientapp.cfg.SERVER_META_URL = 'https://ophostname.com/server/meta/url' - clientapp.add_config_from_json = MagicMock(name='add_config_from_json') - clientapp.add_config_from_json.return_value(None) - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - builtins.open = MagicMock(name='open') - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - clientapp.add_config_from_json = self.stashed_add_config_from_json - - def test_create_app_should_exist(self): - self.assertEqual(hasattr(clientapp, 'create_app'), True, - 'app factory does not exists') - - def test_create_app_should_be_invokable(self): - self.assertEqual(callable(clientapp.create_app), True, - 'cannot invoke create_app from clientapp') - - def test_create_app_should_return_a_flask_app(self): - - self.assertIsInstance(clientapp.create_app(), Flask, - 'create_app is not returning a Flask instance') - - def test_if_app_has_secret_key(self): - self.assertTrue(hasattr(clientapp.create_app(), 'secret_key'), ) - - def test_if_secret_key_not_none(self): - self.assertIsNotNone(clientapp.create_app().secret_key, - 'app secret key is unexpectedly None') - - def test_if_oauth_is_app_extension(self): - self.assertTrue('authlib.integrations.flask_client' in - clientapp.create_app().extensions) - - def test_if_settings_py_exists(self): - self.assertTrue(os.path.exists('clientapp/config.py'), - 'File clientapp/config.py does not exist') - - def test_if_op_client_id_exists_in_app_configuration(self): - self.assertTrue('OP_CLIENT_ID' in clientapp.create_app().config, - 'No OP_CLIENT_ID in app.config') - - def test_if_clientapp_has_cfg(self): - self.assertTrue(hasattr(clientapp, 'cfg')) - - def test_if_cfg_is_module_from_configpy(self): - self.assertTrue( - os.path.relpath(clientapp.cfg.__file__) == 'clientapp/config.py') - - ... - - def test_if_OP_CLIENT_ID_is_equal_cfg_CLIENT_ID(self): - self.assertEqual(clientapp.create_app().config['OP_CLIENT_ID'], - clientapp.cfg.CLIENT_ID) - - def test_if_OP_CLIENT_SECRET_exists_in_app_configuration(self): - self.assertTrue('OP_CLIENT_SECRET' in clientapp.create_app().config, - 'No OP_CLIENT_SECRET in app.config') - - def test_if_OP_CLIENT_SECRET_is_equal_cfg_CLIENT_ID(self): - self.assertEqual(clientapp.create_app().config['OP_CLIENT_SECRET'], - clientapp.cfg.CLIENT_SECRET) - - def test_if_has_attr_ssl_verify(self): - self.assertTrue(hasattr(clientapp, 'ssl_verify'), - 'There is no ssl_verify in clientapp') - - def test_should_have_method_to_set_CA_CURL_CERT(self): - self.assertTrue(clientapp.ssl_verify.__call__) diff --git a/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py b/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py deleted file mode 100644 index 4f7fc9dc13f..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py +++ /dev/null @@ -1,46 +0,0 @@ -import clientapp -from helper import FlaskBaseTestCase - - -class TestPreselectedProvider(FlaskBaseTestCase): - - # """ - # We should be able to send Preselected passport provider to gluu OIDC as a authorization param - # like this: preselectedExternalProvider= - # Where is the Base64-encoded representation of a small JSON - # content that looking like this: - # { "provider" : } - # """ - def setUp(self): - clientapp.cfg.PRE_SELECTED_PROVIDER = True - FlaskBaseTestCase.setUp(FlaskBaseTestCase) - - def test_config_should_have_preselected_provider_option(self): - self.assertTrue(hasattr(clientapp.cfg, 'PRE_SELECTED_PROVIDER'), - 'cfg doesnt have PRE_SELECTED_PROVIDER attribute') - - def test_config_pre_selected_provider_should_be_boolean(self): - self.assertTrue( - type(clientapp.cfg.PRE_SELECTED_PROVIDER) == bool, - 'cfg.PRE_SELECTED_PROVIDER is not bool') - - def test_preselected_provider_id_should_exist_in_cfg(self): - self.assertTrue(hasattr(clientapp.cfg, 'PRE_SELECTED_PROVIDER_ID')) - - def test_clientapp_should_have_get_preselected_provider(self): - self.assertTrue( - hasattr(clientapp, 'get_preselected_provider'), - 'client app does not have get_preselected_provider attr') - - def test_get_preselected_provider_should_be_callable(self): - self.assertTrue(callable(clientapp.get_preselected_provider), - 'get_preselected_provider is not callable') - - def test_get_selected_provider_should_return_base64(self): - - clientapp.cfg.PRE_SELECTED_PROVIDER_ID = 'saml-emaillink' - expected_response = "eyAicHJvdmlkZXIiIDogInNhbWwtZW1haWxsaW5rIiB9" - self.assertEqual(clientapp.get_preselected_provider(), - expected_response) - - diff --git a/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py b/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py deleted file mode 100644 index c0bb85bfc87..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py +++ /dev/null @@ -1,65 +0,0 @@ -import clientapp -from helper import FlaskBaseTestCase, app_endpoints -from flask import url_for, session -from urllib import parse -from clientapp import config as cfg - -class TestLogoutEndpoint(FlaskBaseTestCase): - def authenticated_session_mock(self): - with self.client.session_transaction() as session: - session['id_token'] = 'id_token_stub' - - def test_endpoint_exists(self): - self.assertIn( - 'logout', - app_endpoints(clientapp.create_app()) - ) - - def test_endpoint_should_require_authentication(self): - ... - def test_logout_endpoint_should_redirect_to_home_if_unauthenticated(self): - # print(self.client.get(url_for('logout')).response) - response = self.client.get(url_for('logout')) - assert(response.status_code == 302) - assert(response.location == url_for('index')) - - - def test_logout_endpoint_should_clear_session(self): - with self.client.session_transaction() as sess: - sess['id_token'] = 'id_token_stub' - sess['user'] = 'userinfo stub' - - with self.client: - self.client.get(url_for('logout')) - assert 'id_token' not in session - assert 'user' not in session - - def test_endpoint_should_redirect_to_end_session_endpoint(self): - with self.client.session_transaction() as session: - session['id_token'] = 'id_token_stub' - session['user'] = 'userinfo stub' - - response = self.client.get(url_for('logout')) - - parsed_location = parse.urlparse(response.location) - assert parsed_location.scheme == 'https' - assert parsed_location.netloc == 'ophostname.com' - assert parsed_location.path == '/end_session_endpoint' - - - - def test_endpoint_should_redirect_to_end_session_endpoint_with_params(self): - token_stub = 'id_token_stub' - with self.client.session_transaction() as session: - session['id_token'] = token_stub - session['user'] = 'userinfo stub' - - parsed_redirect_uri = parse.urlparse(cfg.REDIRECT_URIS[0]) - post_logout_uri = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - - expected_query = 'post_logout_redirect_uri=%s&token_hint=%s' % (post_logout_uri, token_stub) - response = self.client.get(url_for('logout')) - - parsed_location = parse.urlparse(response.location) - assert parsed_location.query == expected_query - diff --git a/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py b/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py deleted file mode 100644 index 68f7012c88e..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py +++ /dev/null @@ -1,68 +0,0 @@ -from clientapp import create_app, session -from flask import Flask, url_for -from typing import List -from werkzeug import local -from helper import FlaskBaseTestCase - - -def app_endpoint(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -class TestProtectedContentEndpoint(FlaskBaseTestCase): - def test_app_should_contain_protected_content_route(self): - - endpoints = app_endpoint(create_app()) - self.assertIn('protected-content', endpoints, - 'protected-content route not found in app endpoints') - - def test_app_protected_content_route_should_return_valid_requisition(self): - - self.client.get(url_for('protected_content')) - - self.assertIn( - self.client.get(url_for('protected_content')).status_code, - range(100, 511), - 'protected content route returned invalid requisition') - - def test_should_return_if_session_exists_in_clientapp(self): - import clientapp - self.assertTrue(hasattr(clientapp, 'session'), - "session is not an attribute of clientapp") - del clientapp - - def test_should_check_if_session_is_LocalProxy_instance(self): - self.assertIsInstance(session, local.LocalProxy) - - def test_protected_content_return_status_200_ir_session_profile_exists( - self): - - with self.client.session_transaction() as sess: - sess['user'] = 'foo' - - self.assertEqual( - self.client.get(url_for('protected_content')).status_code, 200) - - def test_should_return_302_if_no_session_profile(self): - self.assertEqual( - self.client.get(url_for('protected_content')).status_code, 302) - - def test_protected_content_should_redirect_to_login_if_session_profile_doesnt_exist( - self): - - response = self.client.get(url_for('protected_content')) - self.assertTrue(response.location.endswith(url_for('login')), - 'Protected page is not redirecting to login page') - - ''' TODO - def test_should_return_if_user_logged_in_exists(self): - self.assertTrue( - hasattr(app,'user_logged_in') - ) - ''' diff --git a/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md b/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md index 57d0a9597f6..df56dce9e18 100644 --- a/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md +++ b/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md @@ -387,7 +387,7 @@ Server deployment ## Test -1. [Setup](https://github.com/JanssenProject/jans/tree/main/demos/jans-tent) Janssen Tent +1. [Setup](https://github.com/JanssenProject/jans/tree/v1.2.0/demos/jans-tent) Janssen Tent 2. Change the configuration as given below in `config.py` ```